BenMcLean981
BenMcLean981

Reputation: 828

Type narrowing with conditional list of keys

I have the following code:

type AlphaNumeric = string | number | null | boolean | undefined;

type AlphaNumericKeys<T> = {
    [key in keyof T]: key extends string ? (T[key] extends AlphaNumeric ? key : never) : never;
}[keyof T];

This works perfectly fine, of a generic object T it returns all the keys of T whose associated value is what I call AlphaNumeric (Im using this for sorting an array based on some keys).

For example, the Alpha numeric keys of a person are as follows:

type Person = {
    name: string;
    age: number;
    friends: Person[];
    doSomething: Function;
}
type PersonAlphaNumericKeys = AlphaNumericKeys<Person> // "name" | "age"

This works perfect right now, the only problem is when I use those keys on T.

type AlphaNumericValuesOfPerson = Person[AlphaNumericKeys<Person>] // string | number

This kind of makes sense at a glance, but when I use a generic it breaks.

type SomeValues<T> = T[AlphaNumericKeys<T>] // T[AlphaNumericKeys<T>], not AlphaNumeric like I would expect.

How can I get T[AlphaNumericKeys] to be the same type as (or at least assignable to) AlphaNumeric? If I have a function that takes AlphaNumeric I want to be able to pass T[AlphaNumericKeys] to it.

Upvotes: 1

Views: 34

Answers (1)

jcalz
jcalz

Reputation: 327859

Clearly SomeValues<T> can be narrower than AlphaNumeric, so you wouldn't want it to evaluate to AlphaNumeric directly:

type S = SomeValues<{ a: 1, b: "foo", c: false, d: Date }>;
// type S = false | 1 | "foo" // a proper subtype of AlphaNumeric

But you are not happy that the generic SomeValues<T> is not seen as extending AlphaNumeric when T has not yet been specified:

function oops<T>(x: SomeValues<T>) {
    const y: AlphaNumeric = x; // error!
    // Type 'SomeValues<T>' is not assignable to type 'AlphaNumeric'.
}

This is just a design limitation of TypeScript. See microsoft/TypeScript#30728 for details. The core of SomeValues is the conditional type of the form (T[K] extends AlphaNumeric ? ...). When the compiler sees that a conditional type depends on an as-yet-unspecified type parameter like T is, it often just defers evaluating that type... and when this happens the compiler cannot really verify that anything specific is assignable to it. It just cannot perform the higher-order reasoning necessary to make the connection between SomeValues<T> as AlphaNumeric.


In your case, if you'd like to work around it and tell the compiler that SomeValues<T> will always be assignable to AlphaNumeric, you could do so by modifying its definition:

type SomeValues<T> = Extract<T[AlphaNumericKeys<T>], AlphaNumeric>

This is using the Extract utility type to filter T[AlphaNumericKeys<T>] to only include those union members assignable to AlphaNumeric. This will be a no-op for any specific value of T:

type S = SomeValues<{ a: 1, b: "foo", c: false, d: Date }>;
// type S = false | 1 | "foo" // still the same

but the compiler will accept that Extract<X, Y> is assignable to Y no matter what X is, thus solving the problem with assignability:

function okay<T>(x: SomeValues<T>) {
    const y: AlphaNumeric = x; // okay
}

Playground link to code

Upvotes: 1

Related Questions