4

I want a type that will yield the keys of another type whose properties match a given type:

type KeyOfType<T, U extends T[keyof T]> = keyof T; // ?? type such that T[KeyOfType<T, U>] == U

For example, consider the interface I:

interface I {
    a: string;
    b: number|string;
    c: string|number;
    d: string;
}

I want the following equivalencies:

type T1 = KeyOfType<I, string>;        // 'a'|'d'
type T2 = KeyOfType<I, string|number>; // 'b'|'c'
type T3 = KeyOfType<I, number>;        // never

I got close with the following:

type KeyOfTypeTest<T, U> = NonNullable<{
    [K in keyof T]: U extends T[K] ? T[K] extends U ? K : never : never;
}[keyof T]>;

This works properly if there are no OR types involved:

type T4 = KeyOfTypeTest<I, string>;        // 'a'|'d' as expected

But fails with union types:

type T5 = KeyOfTypeTest<I, string|number>; // expected 'b'|'c', but got 'a'|'d'

TS Playground link

Any idea what I'm doing wrong? Thanks!

Larry
  • 95
  • 8

1 Answers1

9

Your problem is that the check U extends T[K] ? ... : ... is accidentally a distributive conditional type. Since U is a bare generic type parameter, the compiler splits it into its union constituents, evaluates the conditional type for each one, and produces the the union of the results. In many situations this is desirable behavior, but it's not what you want.

To prevent this from happening, you have to "clothe" the bare type parameter by applying some type function to it. The tersest way to do this is to change U extends T[K] ? ... to [U] extends [T[K]] ? ...:

type KeyOfTypeTest<T, U> = NonNullable<{
    [K in keyof T]: [U] extends [T[K]] ? T[K] extends U ? K : never : never;
}[keyof T]>;

And verify that it does what you want:

type T4 = KeyOfTypeTest<I, string>;        // 'a'|'d' 
type T5 = KeyOfTypeTest<I, string | number>; // 'b'|'c'

Details: the reason it works to wrap both sides of extends in the one-element tuple operator is because array and tuple types are covariant in TypeScript and covariant type operators preserve the extends relationship; if you have a covariant operator F<T>, then X extends Y if and only if F<X> extends F<Y>. If you used some non-covariant type operator you'd still turn off distributive conditional type checking, but you'd also break the check you were doing. For example, in type G<X> = (x: T)=>void, G is not a covariant operator, and so G<U> extends G<T[K]> would not work the way you want it to.


Anyway, I usually call this sort of "keys whose values match some type" operation KeysMatching<T, V>, where you find keys of type T which "match" V in some way, as in this answer.

It's important to think about whether you're trying to support reading properties from T and assigning them to a variable of type V, or reading from a value of type V and assigning it to properties of T, or both. Those three options lead to three different definitions:

type KeysAssignableTo<T, V> =
    { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
type KeysAssignableFrom<T, V> =
    { [K in keyof T]-?: [V] extends [T[K]] ? K : never }[keyof T];
type KeysAssignableBothToAndFrom<T, V> =
    { [K in keyof T]-?: [T[K], V] extends [V, T[K]] ? K : never }[keyof T];

type X = KeysAssignableTo<I, string | 3> // "a" | "d"
// props "a" | "d" can be assigned to a variable of type string | 3

type Y = KeysAssignableFrom<I, string | 3> // "b" | "c"
// a variable of type string | 3 can be assigned to a props "b" | "c"

type Z = KeysAssignableBothToAndFrom<I, string | 3> // never
// you can't both read and write a value of type string | 3 to and from any props

The operation you're doing is the same as KeysAssignableBothToAndFrom:

type T4b = KeysAssignableBothToAndFrom<I, string> // "a" | "d"
type T5b = KeysAssignableBothToAndFrom<I, string | number> // "b" | "c"

If you really need that sort of mutual assignability, great. Otherwise you might want to consider whether your use cases would be better suited to one of the other definitions.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 2
    This answer is fantastic, thank you! Very clearly laid out, nice examples, links to documentation, and alternate suggestions depending on my need. Much appreciated! – Larry Aug 31 '21 at 14:44