Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In TypeScript, how to get the keys of an object type whose values are of a given type?

I've been trying to create a type that consists of the keys of type T whose values are strings. In pseudocode it would be keyof T where T[P] is a string.

The only way I can think of doing this is in two steps:

// a mapped type that filters out properties that aren't strings via a conditional type type StringValueKeys<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };  // all keys of the above type type Key<T> = keyof StringValueKeys<T>; 

However the TS compiler is saying that Key<T> is simply equal to keyof T, even though I've filtered out the keys whose values aren't strings by setting them to never using a conditional type.

So it is still allowing this, for example:

interface Thing {     id: string;     price: number;     other: { stuff: boolean }; }  const key: Key<Thing> = 'other'; 

when the only allowed value of key should really be "id", not "id" | "price" | "other", as the other two keys' values are not strings.

Link to a code sample in the TypeScript playground

like image 492
Aron Avatar asked Feb 04 '19 16:02

Aron


People also ask

How do you get the key of a type?

How to get the keys of a TypeScript interface? To get the union of the keys of a TypeScript interface, we can use the keyof keyword. interface Person { name: string; age: number; location: string; } type Keys = keyof Person; to create the Keys type by setting it to keyof Person .

How do you use object values as TypeScript?

To create a type from an object's values: Use a const assertion when declaring the object. Use keyof typeof to get a type that represents the object's keys. Index the object's type at the specific keys to get a type of its values.

What does Keyof do in TypeScript?

keyof is a keyword in TypeScript which is used to extract the key type from an object type.


2 Answers

This can be done with conditional types and indexed access types, like this:

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

and then you pull out the keys whose properties match string like this:

const key: KeysMatching<Thing, string> = 'other'; // ERROR! // '"other"' is not assignable to type '"id"' 

In detail:

KeysMatching<Thing, string> ➡  {[K in keyof Thing]-?: Thing[K] extends string ? K : never}[keyof Thing] ➡  {    id: string extends string ? 'id' : never;    price: number extends string ? 'number' : never;   other: { stuff: boolean } extends string ? 'other' : never; }['id'|'price'|'other'] ➡  { id: 'id', price: never, other: never }['id' | 'price' | 'other'] ➡  'id' | never | never ➡  'id' 

Note that what you were doing:

type SetNonStringToNever<T> = { [P in keyof T]: T[P] extends string ? T[P] : never }; 

was really just turning non-string property values into never property values. It wasn't touching the keys. Your Thing would become {id: string, price: never, other: never}. And the keys of that are the same as the keys of Thing. The main difference with that and KeysMatching is that you should be selecting keys, not values (so P and not T[P]).

Playground link to code

like image 92
jcalz Avatar answered Sep 19 '22 21:09

jcalz


As a supplementary answer:

Since version 4.1 you can leverage key remapping for an alternative solution (note that core logic does not differ from jcalz's answer). Simply filter out keys that, when used to index the source type, do not produce a type assignable to the target type and extract the union of remaining keys with keyof:

type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P };  interface Thing {     id: string;     price: number;     test: number;     other: { stuff: boolean }; }  type keys1 = KeysWithValsOfType<Thing, string>; //id -> ok type keys2 = KeysWithValsOfType<Thing, number>; //price|test -> ok 

Playground


As rightfully mentioned by Michal Minich:

Both can extract the union of string keys. Yet, when they should be used in more complex situation - like T extends Keys...<T, X> then TS is not able to "understand" your solution well.

Because the type above does not index with keyof T and instead uses keyof of the mapped type, the compiler cannot infer that T is indexable by the output union. To ensure the compiler about that, one can intersect the latter with keyof T:

type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P } & keyof T;  function getNumValueC<T, K extends KeysWithValsOfType<T, number>>(thing: T, key: K) {     return thing[key]; //OK } 

Updated Playground

like image 31
Oleg Valter is with Ukraine Avatar answered Sep 19 '22 21:09

Oleg Valter is with Ukraine