Reputation: 1490
I have a typescript function that takes a generic type and a key for that generic type. I constrain that key so that the function will only accept keys where the values will be of a certain type. When accessing the generic object using the constrained key I don't get the expected type in return.
How do you constrain a key of a generic object to a specific value type and access that value in a generic function?
For example:
function onlyTakesADateKey<T, K extends keyof T>(item: T, key: T[K] extends Date ? K : never): void {
//I've constrained `key` to ensure that item[key] is a Date but this doesn't get typed as a Date
const shouldBeADateButIsNot = item[key]
//Property 'getTime' does not exist on type 'T[T[K] extends Date ? K : never]'.ts(2339)
shouldBeADateButIsNot.getTime()
}
const foo = { key1: "asdf", key2: new Date() }
//Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345)
const bar = onlyTakesADateKey(foo, "key1")
//It properly constrains the key, but per above can't access the date value in the function
const baz = onlyTakesADateKey(foo, "key2")
Why isn't shouldBeADateButIsNot
a Date
? The key is properly constrained. I cannot pass arguments to the function that result it in not being a Date.
Upvotes: 7
Views: 5686
Reputation: 328362
The compiler isn't really able to do much with conditional types that depend on as-yet-unspecified generic type parameters like T
and K
inside the implementation of onlyTakesADateKey
. Inside the function implementation, evaluation of the type T[K] extends Date ? K : never
is deferred. That's why you see an error about T[T[K] extends Date ? K : never]
. The compiler is unable to do the higher-order reasoning necessary to conclude that it must be assignable to Date
. It's a design limitation of TypeScript, as can be seen in microsoft/TypeScript#30728.
The compiler often defers evaluation of types that depend on unspecified generics, but there are a few places where it can do a bit better. One is: if you have a value of type Record<K, V>
and index into it with K
, the compiler will understand that it is of type V
. So generic lookups are not always completely deferred. This suggests rewriting your T
and K
constraints like this:
function onlyTakesADateKey<T extends Record<K, Date>, K extends PropertyKey>(
item: T, key: K): void {
const isActuallyADateNow = item[key]
isActuallyADateNow.getTime()
}
This works without error, and your examples behave similarly:
const foo = { key1: "asdf", key2: new Date() }
const baz = onlyTakesADateKey(foo, "key2"); // okay
with the notable exception that when you make a mistake, the compiler complains about item
and not key
:
const bar = onlyTakesADateKey(foo, "key1"); // error!
// -------------------------> ~~~
// Types of property 'key1' are incompatible.
If you really don't want to change anything about your call, you can always use a type assertion to just tell the compiler what it can't figure out: that shouldBeADateButIsNot
is a Date
:
function onlyTakesADateKeyOrig<T, K extends keyof T>(
item: T, key: T[K] extends Date ? K : never): void {
const shouldBeADateButIsNot = item[key] as any as Date;
shouldBeADateButIsNot.getTime()
}
Upvotes: 6