Home > Software engineering >  Typescript generic union resolution order
Typescript generic union resolution order

Time:10-12

I am trying to create an interface with string Union as the type parameter. It has two children, both are tied to the Union. The idea is that they are coordinated with each other through the type parameter.

Here's a simple repro:

interface IOptions<TValue extends string> {
  values: {[key in TValue]: string};
  items: IItem<TValue>[];
}
interface IItem<TValue extends string> {
  value: TValue;
}

function printOptions<TValue extends string>(options: IOptions<TValue>) {
  console.log(options);
}

printOptions({
  values: {a: 'a', b: 'b'},
  items: [
    { value: 'a'}
  ]
});

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJIHkAOZgHsQDOAPACoBqcANgK4oQAekIAJgcgWFKAOYB8yAbwBQyZADcqtAgC5BAbQDWEAJ7JQycpIgBdWRy4huAXwDcItZAC2MtKiukKNCLznazRoaEixEKVHYhLBy1kBiZWdk4efmFRCSdZTSd3ISEYahAEHHxkTAMwLGzCYKdQxggWNn1ogApcbDxCWQwG-GIk2l4ASkFzBDbcSggAOkpcbjrWwi6UoTyvQsaCGtjxLRsBOFkAcjhtgBpkACMdo 2jffNgKxs5c1EBNYTkXfPzbSEjGaA

This code produces the following error:

Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.

My guess as to what happens is that typescript decides that items array will be the child that defines what the generic Union is, and then complains that values has unknown properties.

My question is, can I somehow make typescript use values as the "union definer" type? Or in some other way make it so that values can have keys that don't appear in items?

CodePudding user response:

You just need to infer each key and value of the argument. COnsider this example:

interface Option<Values, Items> {
  values: Values;
  items: Items;
}

type Item<Value extends PropertyKey> = { value: Value }

function printOptions<
  Value extends string,
  Values extends Record<Value, Value>,
  InferedItem extends Item<keyof Values>,
  Items extends InferedItem[]
>(options: Option<Values, Items>) {
}

printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'a' }
  ]
});


printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'c' } // error
  ]
});

Playground

Treat it as a type destuction. If you want to infer each value, you should destructure it. In other words create the result type from the bottom to top.

  1. First of all, you need to infer key/value of values proeprty: Value extends string

  2. Then, you need to infer whole values property: Values extends Record<Value, Value>

  3. Same approach with items. You need to infer one item: InferedItem extends Item<keyof Values>

  4. Then you can infer all items: Items extends InferedItem[]

If you are interested in Type Inference on function arguments you can take a look on my article

If you want to forbid using values keys in items array, you need to provida validation type helper. You can find more explanation and examples in my article/blog.

interface Option<Values, Items> {
  values: Values;
  items: Items;
}

type Item<Value extends PropertyKey> = { value: Value }

type Check<
  Values extends Record<string, string>,
  Items extends Array<any>,
  Cache extends Array<any> = []
  > =
  Items extends []
  ? Cache
  : Items extends [infer Head, ...infer Tail]
  ? Head extends Item<infer Value>
  ? Value extends keyof Values
  ? Check<Values, Tail, [...Cache, Item<never>]>
  : Check<Values, Tail, [...Cache, Item<Value>]>
  : 1
  : Items;


function printOptions<
  Value extends string,
  ItemValue extends string,
  Values extends Record<Value, Value>,
  InferedItem extends Item<ItemValue>,
  Items extends InferedItem[],
  >(options: Option<Values, Check<Values, [...Items]>>) {
}


printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'c' }, // ok
    { value: 'a' }, // error
  ]
});

Playground

Validation algorithm: iterate through items tuple and check every value whether it extends any key from values. If yes - replace value:char with value:never, otherwise don't do anything with item.

Check returns validated items tuple which is used as a second argument for Options. Now, we ended up with a tuple, where each invalid value is replaved by never. SInce never is unrepresentable it is highlighted by TS compiler.

CodePudding user response:

After looking through captain-yossarian's solution, I came up with this, which works better in my case.

type IValuesLookup = {[key: string]: string};
type StringKeys<T> = keyof T extends string ? keyof T : never;

interface IOptions<TValues extends IValuesLookup, TItem extends IItem<StringKeys<TValues>>> {
  values: TValues;
  items: TItem[];
}
interface IItem<TValue extends string> {
  value: TValue;
}

function printOptions<TValues extends IValuesLookup, TItem extends IItem<StringKeys<TValues>>>(options: IOptions<TValues, TItem>) {
  console.log(options);
}

printOptions({
  values: {a: 'a', b: 'b'},
  items: [
    { value: 'a'}
  ]
});

https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgagQwDYFcIGcAyB7bBrFMKAXigG8BtPCEALinWACcBLAOwHMBdex1zgL4BuAFChIUAMrN2HANI10AHgAqAPhJRqIbADMoKqBAAewCGwAm6BjM5QA-Fpp6DUemwgA3CE1Ej2Zky6CADG0DAA8mDALNhsyiqIqBhGpuZWsEloWLgEYAA0BjBmALapZpbWMMUQJUrS-PKKqlkYau3kIlBQnsjZ9Il9GKLdLKXoAzUlFFyiAv5sgcFhsFMtQ Xp1nyyGmRdPUMDrXMiIroobCExcVBg-MBRN-HrydYmFRnwQzn4hIUqKabSqrUr1WxNEAJVrodpqAAU2GisXi9EiyLi0J AKmagAlJ1uiFMdgkBAAHRIbAcREY J404ie4BJ4o9Dw-bdXpvehkBD0ADkCAFhQARoLRQKBPkDmNahMoBQDt0yIdkoLhfNulwRAIGUA

  • Related