imagio
imagio

Reputation: 1490

Typescript access value of generic type using key constrained by the type of that value

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

Answers (1)

jcalz
jcalz

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()
}

Playground link to code

Upvotes: 6

Related Questions