I have a problem with typings in my code - union type constraint is generated instead of picking only one type from ItemProperties
types dictionary. Is there any solution to make strict constraint for props
property inside ItemConfig
based on current ItemType
?
P. S. wrapping T
generic with tuple at props
type declaration (as said here) not solves the problem.
Simplified code example:
enum ItemType {
Dog = 1,
Car = 2,
Building = 3,
}
interface ItemProperties {
[Item.Dog]: {
name: string;
};
[Item.Car]: {
power: number;
};
}
interface ItemConfig<T extends ItemType = ItemType> {
type: T;
props: T extends keyof ItemProperties ? ItemProperties[T] : {};
}
const config: ItemConfig[] = [
{
type: ItemType.Dog,
props: ...
}
];
Expected typeof props
:
{ name: string }
Actual typeof props
:
{ name: string } | { power: number } | {}
CodePudding user response:
As remarked in the comments, the issue is that ItemConfig
is equal to {type: ItemType, props: {} | {name: string} | {power: number}}
whereas you'll want it to be a discriminating union {type: ItemType.Dog, props: {name: string}} | {type: ItemType.Car, props: {power: number}} | ..
to type the array element props
correctly.
One way to create this union is by using a distributive conditional type (docs):
type ItemConfig<T = ItemType> = T extends ItemType ? {
type: T;
props: T extends keyof ItemProperties ? ItemProperties[T] : {};
} : never
Because the T
in the condition is a naked type parameter, ItemConfig<ItemType.Dog | ItemType.Car | ItemType.Building>
distributes to ItemConfig<ItemType.Dog> | ItemConfig<ItemType.Car> | ItemConfig<ItemType.Building>
, which is the desired union.
Alternatively (as captain-yossarian remarked), since ItemType
extends PropertyKey
(i.e. string | number | symbol
), you can use a mapped type to create an object that has the constituents of the desired union as its values, and index that object to obtain the union:
type ItemConfig = {
[T in ItemType]: {
type: T,
props: T extends keyof ItemProperties ? ItemProperties[T] : {}
}
}[ItemType]
This has the advantage that you don't need ItemConfig
to have a generic parameter T
, but it is limited to types that extend PropertyKey
(otherwise you can't use it as a key in the mapped type).
Both approaches approaches yield the same ItemConfig
union, which will allow the appropriate props
type to be inferred for each array element:
const config: ItemConfig[] = [
{
type: ItemType.Dog,
props: {name: 'x'}
// type of props: {name: string}
},
{
type: ItemType.Car,
props: {power: 7}
// type of props: {power: number}
},
{
type: ItemType.Dog,
props: {power: 7} // Type error
// type of props: {name: string}
}
];