Inferring Types in Conditional Types

In addition to TypeScript 2.8's new feature called conditional types which I coverd in the last post, TypeScript 2.8 also adds a new keyword called infer . It allows inferring and extracting a type inside of a conditional statement. This is really exciting as it allows us to pattern match on type parameters which was not possible in TypeScript up until now!

To revisit, conditional types essentially give you an if statement along with the ability to ask questions at the type level. This enables some very powerful constructs at the type level. Conditional types look as follows:

T extends U ? X : Y

Now we can use the infer keyword in the extends clause. With it, we can infer a type and give it a name and we can use that name in the then clause (the part between ? and : ) of the conditional type. This allows us for example to extract the return type of a function (this previously required an ugly hack and couldn't be done with just a type operator in TypeScript):

type ReturnType<T> = T extends (...args: any []) => infer R ? R : never;

With the extends clause, we check if T is a subtype of a function type returning R and if so, the resulting type becomes R , the return type of a function. It evaluates as follows:

type T0 = ( x1: string , x2: number ) => string []; ReturnType<T0> = ReturnType<(> = ( ( x1: string , x2: number ) => string [] extends ( ...args: any [] ) => infer R ) ? R : never = ( ( x1: string , x2: number ) => string [] extends ( ...args: any [] ) => string [] ) ? string [] : never = string [];

In a similar vein, we can also extract the argument types of a function

FirstArgType<T> = T extends (x: infer X, ...args: any []) => any ? X : never; SecondArgType<T> = T extends (x: any , y: infer Y, ...args: any []) => any ? Y : never;

Inferring union and intersection types

It is possible to use the same type variable in multiple positions as well. Consider the following:

type ParameterType<T> = T extends { a: infer U, b: infer U } ? U : never; type T1 = ParameterType<{ a: number , b: number }>; type T2 = ParameterType<{ a: string , b: string }>; type T3 = ParameterType<{ a: number , b: string }>;

At first it may seem that in the third example the condition should not hold and that thus T3 should be never , however TypeScript can use a union type which allows both number and string . So it will generally do that for types which are covariant.

Similarly, it will infer intersection types for type variables in contravariant positions (generally, these are argument types):

type ArgumentType<T> = T extends (x: infer U, y: infer U) => any ? U : never; type T4 = ArgumentType< ( x: number , y: number ) => any >; type T5 = ArgumentType< ( x: string , y: string ) => any >; type T6 = ArgumentType< ( x: number , y: string ) => any >;

The merged pull request by Anders Hejlsberg's introducing the infer keyword has some additional details on how conditional types are evaluated, if you want to dig even deeper.

I am very excited that we can now infer and extract types as this makes a lot of things much simpler in type level programming in TypeScript!

Please enable JavaScript to view the comments powered by Disqus.

Disqus