Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keyof that is also of type T

Tags:

typescript

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?

like image 647
Robus Avatar asked Apr 12 '18 12:04

Robus


3 Answers

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
like image 178
Motti Avatar answered Nov 02 '22 21:11

Motti


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
like image 42
Gobius Dolhain Avatar answered Nov 02 '22 19:11

Gobius Dolhain


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!

like image 41
jcalz Avatar answered Nov 02 '22 19:11

jcalz