Is there a way to pass an array of generic types so that the generic type of each element is inferred?

55 views Asked by At

Consider the following:

  type ValueMap = {
    foo: 'FOO1' | 'FOO2';
    bar: 'BAR1' | 'BAR2' | 'BAR3';
  };
  type Key = keyof ValueMap;

  type Value<K extends Key> = ValueMap[K];

  type Entry<K extends Key> = {
    key: K;
    value: Value<K>;
  }

In other words, an Entry consists of a key and a value, and the set of allowed values depends on the key, so e.g.

{key: 'foo', value: 'FOO2'} - valid
{key: 'foo', value: 'BAR3'} - invalid
{key: 'bar', value: 'BAR1'} - valid
{key: 'bar', value: 'FOO2'} - invalid

Now, I want to send a set of such entries to a function, and I want the type of each entry to be inferred from that element's key. However, If I do this as an array, this doesn't work very well:

  function sendArray<K extends Key>(v: Array<Entry<K>>) {
    ...
  }

  sendArray([{key: 'foo', value: 'FOO2'}, {key: 'bar', value: 'BAR1'}])
  sendArray([{key: 'foo', value: 'FOO2'}, {key: 'bar', value: 'FOO1'}]) // compiles :(

Here, sendArray is called with K being inferred to "foo" | "bar", and each array element is validated only so that key is assignable to "foo" | "bar" and value is assignable to ValueMap<"foo" | "bar">, i.e., values from foo are valid for bar entries, and vice versa. :(

I know that if I use a record type instead, it works:

  type Entries<K extends Key> = {
    [P in K]?: Value<P>;
  }

  function sendRecord<K extends Key>(v: EntryRecord<K>) {
  }

  sendRecord({foo: 'FOO2', bar: 'BAR3'})
  sendRecord({foo: 'FOO2', bar: 'FOO1'}) // does not compile! :) 

... which is nice, but I'd really want this to work for an array. Is there some way to do this, so that the first array element in inferred to be a Entry<"foo"> and the second element is inferred to be a Entry<"bar">?

2

There are 2 answers

1
wonderflame On BEST ANSWER

You don't need generics here at all, but if for some reason you still need them outside of this exact issue we will still have to make Entry nongeneric. The desired type is:

{
    key: "foo";
    value: "FOO1" | "FOO2";
} | {
    key: "bar";
    value: "BAR1" | "BAR2" | "BAR3";
}

To achieve this we will use the mapped types:

type Entry = {
  [K in Key]: {
    key: K;
    value: ValueMap[K];
  };
}[Key];

Then, in the sendArray we have to modify the generic to accept the Entry[], not Key. And to make sure that read-only arrays are also accepted we will add readonly to the constraint:

function sendArray<E extends readonly Entry[]>(v: E) {}

Testing:

sendArray([
  { key: "foo", value: "FOO2" },
  { key: "bar", value: "BAR1" },
]);
sendArray([
  { key: "foo", value: "FOO2" },
  { key: "bar", value: "FOO1" },
]); // doesn't compile :)

playground

1
Behemoth On

As every Entry object in the array argument must be type checked individually you'll need a constraint that does just that. In the following example I used an additional generic createEntry function which ensures that no mismatching keys and values can used in the same object. So you basically type-check each entry separately.

type ValueMap = {
  foo: "FOO1" | "FOO2";
  bar: "BAR1" | "BAR2" | "BAR3";
};
type Key = keyof ValueMap;

type Value<K extends Key> = ValueMap[K];

type Entry<K extends Key> = {
  key: K;
  value: Value<K>;
};

function createEntry<K extends Key>(key: K, value: Value<K>): Entry<K> {
  return { key, value };
}
declare function sendArray<K extends Key>(v: Entry<K>[]): void;

sendArray([createEntry('foo', 'FOO1'), createEntry('bar', 'BAR1')]);
sendArray([createEntry('foo', 'BAR1'), createEntry('bar', "FOO1")]);
//                            ~~~~~~                      ~~~~~~

TypeScript Playground