Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a subset of `keyof T` whose value, T[K] are callable functions in Typescript

Tags:

typescript

I want to filter keyof T based on type of T[keyof T]

It should work like this:

type KeyOfType<T, U> = ...

KeyOfType<{a: 1, b: '', c: 0, d: () => 1}, number> === 'a' | 'c'

KeyOfType<{a: 1, b: '', c: 0: d: () => 1}, string> === 'b'

KeyOfType<{a: 1, b: '', c: 0: d: () => 1}, Function> === 'd'

Is this possible?

like image 423
Joon Avatar asked Jul 19 '18 09:07

Joon


People also ask

What is keyof in typescript and how to use it?

Enter TypeScript 2.1 and the new keyof operator. It queries the set of keys for a given type, which is why it's also called an index type query. Let's assume we have defined the following Todo interface: We can apply the keyof operator to the Todo type to get back a type representing all its property keys, which is a union of string literal types:

How to query the set of keys for a given type?

We need to provide a little more type information to make that possible. Enter TypeScript 2.1 and the new keyof operator. It queries the set of keys for a given type, which is why it's also called an index type query. Let's assume we have defined the following Todo interface:

What is typescript and how does it work?

It’s enough to know that TypeScript allows you to take an existing type and slightly modify it to make a new type. This is part of its Turing Completeness. You can think of type as *function *— it takes another type as input, makes some calculations and produces new type as output.

Can typescript infer the return type of a Prop function?

The return type is still inferred to be any, however: Without further information, TypeScript can't know which value will be passed for the key parameter, so it can't infer a more specific return type for the prop function. We need to provide a little more type information to make that possible. Enter TypeScript 2.1 and the new keyof operator.


2 Answers

You can use the as clause of a mapped type to filter the keys of a type:

type KeyOfType<T, V> = keyof {
    [P in keyof T as T[P] extends V? P: never]: any
}

The as clause of a mapped type allows us to manipulate the name of the key. If the key is mapped to never the key gets removed from the resulting type. You can find more info in this PR

Pre TS 4.1

This version still works in post 4.1, just the other way is easier to read.

You can do this using conditional and mapped types

type KeyOfType<T, U> = {[P in keyof T]: T[P] extends U ? P: never}[keyof T]

Let's break things down a little bit.

We can start with a mapped type, that has the same properties of the same type as the original T, this a simple standard mapped type:

type KeyOfType<T> = { [P in keyof T]: T[P] } // New Type same as the original

T[P] is a type query and means the type of the key P in type T. We can change this to just P, meaning that the type of the new property is the same as it's name:

type KeyOfType<T> = { [P in keyof T]: P }
// So
KeyOfType<{ a: number, b: string }> == { a: 'a', b: 'b' }

We can add a type query to this type to again get all the keys of the type. Generally a construct T[keyof T] gets all the property types of a type. Applying this to our mapped type which has the property types the same as the key names we basically get back to keyof T:

type KeyOfType<T> = { [P in keyof T]: P }[keyof T]
// So
KeyOfType<{ a: number, b: string }> ==  'a'|'b' 

Now we can add a conditional type to o not always select P. Since A | never == A we can set the type of the property in the mapped type to never if the type of the original property (T[P]) does not meet a certain constraint.

To express the constraint we add an extra generic parameter U and we use a conditional type which has the form T extends U ? TypeIfTrue: TYpeIfFalse. Putting it together we get:

type KeyOfType<T, U> = {[P in keyof T]: T[P] extends U ? P: never}[keyof T]
like image 174
Titian Cernicova-Dragomir Avatar answered Sep 21 '22 13:09

Titian Cernicova-Dragomir


awesome explanation!!

I would also add that you can set a default type to U to be any key of T like so:

type Keys<T> = {[P in keyof T]: T[P]}[typeof P]

type KeyOfType<T, U = Keys<T>> = {[P in keyof T]: T[P] extends U ? P : never}[keyof T]

I add U = Keys<T> to the generic definition of KeyOfType

Thanks for the explanation!

like image 38
Horacio Herrera Avatar answered Sep 19 '22 13:09

Horacio Herrera