joshuahhh
joshuahhh

Reputation: 233

Trouble with unions of array types

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:

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.

Upvotes: 1

Views: 511

Answers (1)

Matt McCutchen
Matt McCutchen

Reputation: 30919

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.

Upvotes: 2

Related Questions