1

Here are some types I'm using (simplified for this conversation):

export interface NodeId { readonly _nodeId: string }
export interface CellId { readonly _cellId: string }

export type Call = CodeCall | DefinitionCall

export interface CodeCall {
  readonly inputs: Array<{
    readonly outside: NodeId,
    readonly inside: string,
  }>,
}

export interface DefinitionCall {
  readonly inputs: Array<{
    readonly outside: NodeId,
    readonly inside: CellId,
  }>,
}

Key here: CodeCall and DefinitionCall each contain an array of "inputs", with overlapping but different definitions of what an input is.

Here's a useful function for my application:

export function doesCallUseNode1(call: Call, nodeId: NodeId): boolean {
  for (let input of call.inputs) {
    if (input.outside === nodeId) {
      return true;
    }
  }
  return false;
}

This works! But gosh, it would be nice to use a utility function to do the search. Here's the signature of a utility function I like:

declare function findWith<T, K extends keyof T>(arr: T[], key: K, value: T[K]): boolean;

But if I try to use it like this,

export function doesCallUseNode2(call: Call, nodeId: NodeId): boolean {
  return findWith(call.inputs, "outside", nodeId)
}

I get an error! In particular, this error:

Argument of type '{ readonly outside: NodeId; readonly inside: string; }[] | { readonly outside: NodeId; readonly inside: CellId; }[]' is not assignable to parameter of type '{ readonly outside: NodeId; readonly inside: string; }[]'.

My analysis: call.inputs has type {readonly outside: NodeId; readonly inside: string;}[] | {readonly outside: NodeId; readonly inside: CellId;}[]. findWith can be called with either:

  • T = {readonly outside: NodeId; readonly inside: string;}, K = 'outside'
  • T = {readonly outside: NodeId; readonly inside: CellId;}, K = 'outside'

But it can't be called with T = the union of these. I guess this is kinda reasonable – TypeScript has no way of knowing that I'm using arrays in a context in which this should make sense.

I'm stuck figuring out how to type findWith to make this work. Any ideas? (Thanks in advance for any help!)


Update: Thanks to Matt for his helpful answer, below. Just for future reference: I have ended up implementing this as follows (using lodash)...

export function findWith<T>(arr: Array<T>, key: keyof T, value: T[keyof T]): T | undefined {
  return _.find(arr, (o) => _.isEqual(o[key], value))
}

export function hasWith<K extends keyof any, V>(arr: {[key in K]: V}[], key: K, value: V): boolean {
  return !!findWith(arr, key, value)
}

I am relieved that hasWith can be implemented (in the flexible way I want) by calling a stricter findWith, which holds onto more type information for stricter uses.

joshuahhh
  • 183
  • 6

1 Answers1

2

Try this:

declare function findWith<K extends keyof any, V>(arr: {[P in K]: V}[], key: K, value: V): boolean;

Then, instead of trying to match T[] against {readonly outside: NodeId; readonly inside: string;}[] | {readonly outside: NodeId; readonly inside: CellId;}[] and getting two conflicting inferences for T, you just require that the array have the key you are looking for, which it does for both cases of the union.

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • Thanks a bunch! I considered something like this, but had missed the "[P in K]" notation. This is great. – joshuahhh Sep 10 '18 at 21:17