Supposed I have a type like this:
type TInfoGeneric<TType extends string, TValue> = {
valueType: TType,
value: TValue, // Correspond to valueType
}
To avoid repeating my self, I create a type map that list the possible valueType and matches valueType with, well, value's type.
type TInfoTypeMap = {
num: number;
str: string;
}
Now, to actually create TInfo, I use mapped type to map all types into TInfoGeneric and then get only value side of it.
type TAllPossibleTInfoMap = {
[P in keyof TInfoTypeMap]: TInfoGeneric<P, TInfoTypeMap[P]>;
};
type TInfo = TAllPossibleTInfoMap[keyof TAllPossibleTInfoMap]; // TInfoGeneric<"num", number> | TInfoGeneric<"str", string>
Then, to define handlers for all types, I create another mapped type just for handlers.
type TInfoHandler = {
[P in keyof TInfoTypeMap]: (value: TInfoTypeMap[P]) => any
};
const handlers: TInfoHandler = {
num: (value) => console.log(value.toString(16)),
str: (value) => console.log(value),
}
And finally, to actually use the handler, I create a function like this:
function handleInfo(info: TInfo) {
handlers[info.valueType](info.value); // Error
}
I got this error:
Argument of type 'string | number' is not assignable to parameter of type 'number & string'.
Type 'string' is not assignable to type 'number & string'.
Type 'string' is not assignable to type 'number'.
Normally, it's understandable that handlers[info.valueType] may be a ((value: number) => any) | ((value: string) => any). However, in this case:
info.valueType is 'num', then we can be sure that handlers[info.valueType] is (value: number) => any) and info.value is number. Thus, handlers[info.valueType] can be called with info.value.info.valueType is 'str', then we can be sure that handlers[info.valueType] is (value: string) => any) and info.value is string. Thus, handlers[info.valueType] can be called with info.value.I'm not sure if this is Typescript limitation or not, but is it possible to write the code in this style so that it's type-checked?
Yeah, there's no convenient and type-safe solution for you here. I've opened an issue at microsoft/TypeScript#30581 about this but I don't expect it to be addressed.
I see two main ways forward. One is to just use a type assertion, since you legitimately know more than the compiler does here. It could be like this:
function handleInfo(info: TInfo) {
// assert your way out. Not type safe but convenient!
(handlers[info.valueType] as (x: number | string)=>any)(info.value);
}
Now there's no error. It's not type safe. But it's convenient and doesn't change the emitted JavaScript.
Or you could try to walk the compiler through the cases and prove to it that all is fine. This is complex, brittle, and has runtime effects:
const typeGuards: {
[P in keyof TInfoTypeMap]: (x: TInfoTypeMap[keyof TInfoTypeMap])=>x is TInfoTypeMap[P];
} = {
num: (x:any): x is number => typeof x === "number",
str: (x:any): x is string => typeof x === "string"
}
function narrowTInfo<K extends keyof TAllPossibleTInfoMap>(
x: TInfo, v: K): x is TAllPossibleTInfoMap[K] {
return typeGuards[v](x.value);
}
function handleInfo(info: TInfo) {
if (narrowTInfo(info, "num")) {
handlers[info.valueType](info.value); // okay
} else {
handlers[info.valueType](info.value); // okay
}
}
That works but is obnoxious. So I'd recommend an assertion.
Hope that helps; good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With