I'm having a little trouble understanding how string enums behave when using them to index types. It seems like sometimes TS recognizes that a string enum's value is an extension of keyof some type, and other times it doesn't. To illustrate:
enum Key {
FOO = "foo",
}
type MyObj = {
foo: string
}
So, an enum of the properties of a type (i.e., you can index into obj:MyObj using obj[Key.FOO]).
Now I want to define another type that will lookup a value in the MyObj type to determine if it is a keyof that type and, if so, use the type of that key's value:
type FunctionReturn<T extends Record<string, any>,U extends keyof T> = {
returnValue: U extends keyof T ? T[U]: never
}
If I use that type as follows, though, I get an error.
function myFunction(value: MyObj[Key.FOO]):FunctionReturn<MyObj, Key.FOO>{
return {returnValue: "a string"}
}
Namely, TS does not find the U extends key of T (where U is an enum member) to be satisfied in typing returnValue even though it accepted it as meeting the constraint of the FunctionReturn type.
The error goes away completely if I pass the actual string literal as the parameter.
function myOtherFunction(value: MyObj[Key.FOO]):FunctionReturn<MyObj, "foo">{
return {returnValue: "a string"}
Why is that? It seems to me if TS excepted Key.FOO as a valid extension of keyof MyOb in the generic constraint, it should also meet the conditional. What am I missing?
Here's the playground.
Much appreciated, as always.
You can use such trick:
enum Key {
FOO = "foo",
}
type MyObj = {
foo: string
}
/*
I've added V generic parameter to tell TS to use U as keyof T, not as something else.
Now it works as expected.
*/
type FunctionReturn<T extends Record<string, any>,U extends keyof T, V = T[U]> = {
returnValue: U extends keyof T ? V: never
}
function myFunction(value: MyObj[Key.FOO]):FunctionReturn<MyObj, Key.FOO>{
return {returnValue: "a string"}
}
function myOtherFunction(value: MyObj[Key.FOO]):FunctionReturn<MyObj, "foo">{
return {returnValue: "a string"}
}
Some explanation:
Let's take it from the beginning. The function expects U extends keyof T as the second type variable. It means you can pass anything that's assignable to keyof T and also it means that U within function execution can be any type that's assignable to keyof T. When you pass Key.FOO as U, TS checks assignability to keyof T and it will be passed. Then, TS infers type of U as Key (that's important thing). Then, because of U extends keyof T will be resolved to true, TS will resolve T[U] to T[Key]. Then, to resolve T[key] TS will make intersection with constraint keyof T, like this T[Key & keyof T], which in turn resolves to T[never]. The most interesting thing is that such intersection Key.FOO & "foo" will be resolved to never. Why to never? Well, probably because of strict rule, like any intersection enum with some other type except enum itself in TS will be resolved to never.
Why suggested approach works:
When you use FunctionReturn like this FunctionReturn<MyObj, Key.FOO>, TS first resolves generic parameters:
T = MyObj,
U = Key.FOO,
V = MyObj[Key.FOO] = string
Because there is no indexed access inside true branch, there is no need to resolve it somehow, TS just uses type string inside true branch.
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