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'}
]
});
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
]
});
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.
First of all, you need to infer
key/value
ofvalues
proeprty:Value extends string
Then, you need to infer whole
values
property:Values extends Record<Value, Value>
Same approach with items. You need to infer one item:
InferedItem extends Item<keyof Values>
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
]
});
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'}
]
});