Is there a way to restrict keyof T
so that it only accepts keys that are of a certain type? Suppose the following:
interface Something {
id: number;
name: string;
value1: FancyType;
value2: FancyType;
value3: FancyType;
}
function someFunction(key: keyof Something) {
// do something to a FancyType
}
someFunction
would accept id | name | value1 | value2 | value3
, is there a way to restrict it to keys of type FancyType
, ie value1 | value2 | value3
?
Since TypeScript 2.8 this is possible with conditional types (Code).
type FancyProperties<T> = Pick<T, {
[K in keyof T]: T[K] extends FancyType ? K : never
}[keyof T]>;
function someFunction(key: keyof FancyProperties<Something>) {
// do something to a FancyType
}
someFunction("id"); // Error
someFunction("value2"); // OK
A better implementation of @Motti's answer is this:
type FilteredKeyOf<T, TK> = keyof Pick<T, { [K in keyof T]: T[K] extends TK ? K : never }[keyof T]>;
It uses the same principle of using conditional types and never
, but parameterizes the the type your properties should extend in TK
.
You can use it with more flexibility:
function someFunction(key: FilteredKeyOf<Something, FancyType>) {
// do something to a FancyType
}
someFunction("id"); // error
someFunction("value2"); // okay
Unfortunately I don't have a TS 2.6.2 compiler in front of me, so I can't double check if this works. And as it involves augmentation to an existing type, it's not my favorite solution either (the 2.8 solution with conditional types suggested by @Motti is definitely the way to go).
That said, here's one way to try it. I'm going to assume that FancyType
is an interface somewhere and that you can merge a property into it. Here it is:
interface FancyType {
'**fancyBrand**'?: never
}
So a value of type FancyType
now is said to have an optional property whose key name is **fancyBrand**
and whose value is of the impossible never
type. In practice that means that you don't need to create such a property, and existing values of type FancyType
will not cause you any type errors.
Now for some helper type functions. ValueOf<T>
just produces the union of property types of an object:
type ValueOf<T> = T[keyof T];
So ValueOf<{a: string, b: number}>
would be string | number
. And Diff
takes two unions of string literals and removes those in the second one from the first one:
type Diff<T extends string, U extends string> =
({ [K in T]: K } & { [K in U]: never } & { [k: string]: never })[T];
So Diff<'a'|'b'|'c', 'a'>
would be 'b'|'c'
.
Finally we're ready to define FancyKeys<T>
, the type function that returns the keys of T
whose property values are FancyType
:
type FancyKeys<T> = Diff<keyof T, ValueOf<
{ [K in keyof T]: (T[K] & { '**fancyBrand**': K })['**fancyBrand**'] }>>
And therefore FancyKeys<Something>
would be 'value1'|'value2'|'value3'
:
function someFunction(key: FancyKeys<Something>) {
// do something to a FancyType
}
someFunction("id"); // error
someFunction("value2"); // okay
FancyKeys<T>
works by essentially intersecting T
with a type whose keys are the same as T
, and whose values each has a '**fancyBrand**'
property equal to the key. For properties of FancyType
type, the resulting '**fancyBrand**'
property will still be never
or undefined
. But for those of any other type (which lacks a '**fancyBrand**'
key), the resulting property will be the key.
This turns Something
into {id: 'id', name: 'name', value1: undefined, value2: undefined, value3: undefined}
. When you take ValueOf<T>
for that, it becomes 'id'|'name'|undefined
. And for some reason I can't quite fathom, it lets you take the Diff<keyof Something, U>
to produce just 'value1'|'value2'|'value2'
... not sure why it allows undefined
in Diff<>
, but I'm not complaining. If it hadn't let us do that, the only viable alternative would be to make the **fancyBrand**
property required in FancyType
, which would make your life more trouble than you want when you try to instantiate it (unless it's a class, in which case it's not a huge deal).
So this works, but feels yucky to me, and I've more or less given up this kind of hacky craziness to use conditional types instead. Still, hope it helps to know about it. Good luck!
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