I have a hard time figuring out how to narrow inferred types in the following example. For now, all format functions accept string | number | boolean
and expect to return string | number | boolean
.
Ideally, I would like to narrow these to only 1 type based on value of typeID
.
enum TypeID {
Number = "__number__",
String = "__string__",
Boolean = "__boolean__"
}
type Type<TYPE_ID extends TypeID> = {
[TypeID.Number]: number;
[TypeID.String]: string;
[TypeID.Boolean]: boolean;
}[TYPE_ID];
type Item<
TYPE_ID extends TypeID,
TYPE extends Type<TYPE_ID> = Type<TYPE_ID>
> = {
typeID: TypeID;
format: (input: TYPE) => TYPE;
};
type Options<TYPE_ID extends TypeID> = Array<Item<TYPE_ID>>;
const someFunc = <TYPE_ID extends TypeID>(options: Options<TYPE_ID>) => {
return null as any;
};
someFunc([
{
typeID: TypeID.Number,
format: input => input // these should have type "number"
},
{
typeID: TypeID.Boolean,
format: input => input // these should have type "boolean"
},
{
typeID: TypeID.String,
format: input => input // these should have type "string"
}
]);
It's pretty confusing to have such similar type names (TYPE
vs Type
vs TYPE_ID
vs TypeID
) so I'm going to make the names distinct in what follows... for generic parameters I use one-or-two capital letters (which is the convention for whatever reason). Here's Type
:
type Type<T extends TypeID> = {
[TypeID.Number]: number;
[TypeID.String]: string;
[TypeID.Boolean]: boolean;
}[T];
And here's Item
:
type Item<T extends TypeID> = {
typeID: T; // <-- T, not TypeID
format: (input: Type<T>) => Type<T>;
};
Note this is modified further from your example. You had two type parameters here, but only seemed to use a default value for the second one, so I removed it and used that default value. But the important modification here is that the typeID
property is of the generic type T
, not the concrete type TypeID
. I think you probably meant to do this in your code, but maybe it was hard to see the difference.
Now the way I'd go from here is to generate a type called SomeItem
which is the union of all possible Item<T>
types. This concrete type can be used more easily than generic types:
type _SomeItem<T extends TypeID> = T extends any ? Item<T> : never;
type SomeItem = _SomeItem<TypeID>;
// type SomeItem = Item<TypeID.Number> | Item<TypeID.String> | Item<TypeID.Boolean>
That worked by using a distributive conditional type in the definition of _SomeItem
, to distribute Item<T>
across the union of TypeID
values.
Finally, someFunc
can be a concrete function:
const someFunc = (options: Array<SomeItem>) => {
return null as any;
};
And when you use it, you get the inference you expect:
someFunc([
{
typeID: TypeID.Number,
format: input => input // has type number
},
{
typeID: TypeID.Boolean,
format: input => input // has type boolean
},
{
typeID: TypeID.String,
format: input => input // has type string
}
]);
Okay, hope that helps. Good luck!
Link to code