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
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 .
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.
keyof is a keyword in TypeScript which is used to extract the key type from an object type.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With