Conditional Types in TypeScript
TypeScript 2.8 introduced conditional types, a powerful and exciting addition to the type system. Conditional types let us express non-uniform type mappings, that is, type transformations that differ depending on a condition.
#Introduction to Conditional Types
A conditional type describes a type relationship test and selects one of two possible types, depending on the outcome of that test. It always has the following form:
T extends U ? X : Y
Conditional types use the familiar ... ? ... : ...
syntax that JavaScript uses for conditional expressions. T
, U
, X
, and Y
stand for arbitrary types. The T extends U
part describes the type relationship test. If this condition is met, the type X
is selected; otherwise the type Y
is selected.
In human language, this conditional type reads as follows: If the type T
is assignable to the type U
, select the type X
; otherwise, select the type Y
.
Here's an example for a conditional type that is predefined in TypeScript's lib.es5.d.ts type definition file:
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
The NonNullable<T>
type selects the never
type if the type T
is assignable to either the type null
or the type undefined
; otherwise it keeps the type T
. The never
type is TypeScript's bottom type, the type for values that never occur.
#Distributive Conditional Types
So why is the combination of a conditional type and the never
type useful? It effectively allows us to remove constituent types from a union type. If the relationship test in the conditional type checks a naked type parameter, the conditional type is called a distributive conditional type, and it is distributed over a union type when that union type is instantiated.
Since NonNullable<T>
checks a naked type parameter, it is distributed over a union type A | B
. This means that NonNullable<A | B>
is resolved as NonNullable<A> | NonNullable<B>
. If e.g. NonNullable<A>
resolves to the never
type, we can remove A
from the resulting union type, effectively filtering out type A
due to its nullability. The same is true for NonNullable<B>
.
This description was fairly abstract, so let's look at a concrete example. We'll define an EmailAddress
type alias that represents a union of four different types, including the null
and undefined
unit types:
type EmailAddress = string | string[] | null | undefined;
Let's now apply the NonNullable<T>
type to EmailAddress
and resolve the resulting type step by step:
type NonNullableEmailAddress = NonNullable<EmailAddress>;
We'll start by replacing EmailAddress
by the union type that it aliases:
type NonNullableEmailAddress = NonNullable<
string | string[] | null | undefined
>;
Here's where the distributive nature of conditional types comes into play. We're applying the NonNullable<T>
type to a union type; this is equivalent to applying the conditional type to all types in the union type:
type NonNullableEmailAddress =
| NonNullable<string>
| NonNullable<string[]>
| NonNullable<null>
| NonNullable<undefined>;
We can now replace NonNullable<T>
by its definition everywhere:
type NonNullableEmailAddress =
| (string extends null | undefined ? never : string)
| (string[] extends null | undefined ? never : string[])
| (null extends null | undefined ? never : null)
| (undefined extends null | undefined ? never : undefined);
Next, we'll have to resolve each of the four conditional types. Neither string
nor string[]
are assignable to null | undefined
, which is why the first two types select string
and string[]
. Both null
and undefined
are assignable to null | undefined
, which is why both the last two types select never
:
type NonNullableEmailAddress = string | string[] | never | never;
Because never
is a subtype of every type, we can omit it from the union type. This leaves us with the final result:
type NonNullableEmailAddress = string | string[];
And that's indeed what we would expect our type to be!
#Mapped Types with Conditional Types
Let's now look at a more complex example that combines mapped types with conditional types. Here, we're defining a type that extracts all non-nullable property keys from a type:
type NonNullablePropertyKeys<T> = {
[P in keyof T]: null extends T[P] ? never : P;
}[keyof T];
This type might seem quite cryptic at first. Once again, I'll attempt to demystify it by looking at a concrete example and resolving the resulting type step by step.
Let's say we have a User
type and want to use the NonNullablePropertyKeys<T>
type to find out which properties are non-nullable:
type User = {
name: string;
email: string | null;
};
type NonNullableUserPropertyKeys = NonNullablePropertyKeys<User>;
Here's how we can resolve NonNullablePropertyKeys<User>
. First, we'll supply the User
type as a type argument for the T
type parameter:
type NonNullableUserPropertyKeys = {
[P in keyof User]: null extends User[P] ? never : P;
}[keyof User];
Second, we'll resolve keyof User
within the mapped type. The User
type has two properties, name
and email
, so we'll end up with a union type with the "name"
and "email"
string literal types:
type NonNullableUserPropertyKeys = {
[P in "name" | "email"]: null extends User[P] ? never : P;
}[keyof User];
Next, we'll unroll the P in …
mapping and substitute "name"
and "email"
for the P
type:
type NonNullableUserPropertyKeys = {
name: null extends User["name"] ? never : "name";
email: null extends User["email"] ? never : "email";
}[keyof User];
We can then go ahead and resolve the indexed access types User["name"]
and User["email"]
by looking up the types of the name
and email
properties in User
:
type NonNullableUserPropertyKeys = {
name: null extends string ? never : "name";
email: null extends string | null ? never : "email";
}[keyof User];
Now it's time to apply our conditional type. null
does not extend string
, but it does extend string | null
— we therefore end up with the "name"
and never
types, respectively:
type NonNullableUserPropertyKeys = {
name: "name";
email: never;
}[keyof User];
We're now done with both the mapped type and the conditional type. Once more, we'll resolve keyof User
:
type NonNullableUserPropertyKeys = {
name: "name";
email: never;
}["name" | "email"];
We now have an indexed access type that looks up the types of the name
and email
properties. TypeScript resolves it by looking up each type individually and creating a union type of the results:
type NonNullableUserPropertyKeys =
| { name: "name"; email: never }["name"]
| { name: "name"; email: never }["email"];
We're almost done! We can now look up the name
and email
properties in our two object types. The name
property has type "name"
and the email
property has type never
:
type NonNullableUserPropertyKeys = "name" | never;
And just like before, we can simplify the resulting union type by purging the never
type:
type NonNullableUserPropertyKeys = "name";
That's it! The only non-nullable property key in our User
type is "name"
.
Let's take this example one step further and define a type that extracts all non-nullable properties of a given type. We can use the Pick<T, K>
type to , which is predefined in lib.es5.d.ts:
/**
* From T, pick a set of properties
* whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
We can combine our NonNullablePropertyKeys<T>
type with Pick<T, K>
to define NonNullableProperties<T>
, which is the type that we were looking for:
type NonNullableProperties<T> = Pick<T, NonNullablePropertyKeys<T>>;
type NonNullableUserProperties = NonNullableProperties<User>;
// { name: string }
And indeed, this is the type we would expect: in our User
type, only the name
property is non-nullable.
#Type Inference in Conditional Types
Another useful feature that conditional types support is inferring type variables using the infer
keyword. Within the extends
clause of a conditional type, you can use the infer
keyword to infer a type variable, effectively performing pattern matching on types:
type First<T> = T extends [infer U, ...unknown[]] ? U : never;
type SomeTupleType = [string, number, boolean];
type FirstElementType = First<SomeTupleType>; // string
Note that the inferred type variables (in this case, U
) can only be used in the true branch of the conditional type.
A long-standing feature request for TypeScript has been the ability to extract the return type of a given function. Here's a simplified version of the ReturnType<T>
type that's predefined in lib.es5.d.ts. It uses the infer
keyword to infer the return type of a function type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // boolean
Note that we have to use typeof
to obtain the return type of the Math.random()
and Array.isArray()
methods. We need to pass a type as an argument for the type parameter T
, not a value; this is why ReturnType<Math.random>
and ReturnType<Array.isArray>
would be incorrect.
For more information on how infer
works, check out this pull request in which Anders Hejlsberg introduced type inference in conditional types.
#Predefined Conditional Types
Conditional types are definitely an advanced feature of TypeScript's type system. To give you some more examples of what they can be used for, I want to go over the conditional types that are predefined in TypeScript's lib.es5.d.ts file.
#The NonNullable<T>
Conditional Type
We've already seen and used the NonNullable<T>
type which filters out the null
and undefined
types from T
.
The definition:
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
Some examples:
type A = NonNullable<boolean>; // boolean
type B = NonNullable<number | null>; // number
type C = NonNullable<string | undefined>; // string
type D = NonNullable<null | undefined>; // never
Note how the empty type D
is represented by never
.
#The Extract<T, U>
Conditional Type
The Extract<T, U>
type lets us filter the type T
and keep all those types that are assignable to U
.
The definition:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
Some examples:
type A = Extract<string | string[], any[]>; // string[]
type B = Extract<(() => void) | null, Function>; // () => void
type C = Extract<200 | 400, 200 | 201>; // 200
type D = Extract<number, boolean>; // never
#The Exclude<T, U>
Conditional Type
The Exclude<T, U>
type lets us filter the type T
and keep those types that are not assignable to U
. It is the counterpart of the Extract<T, U>
type.
The definition:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
Some examples:
type A = Exclude<string | string[], any[]>; // string
type B = Exclude<(() => void) | null, Function>; // null
type C = Exclude<200 | 400, 200 | 201>; // 400
type D = Exclude<number, boolean>; // number
#The ReturnType<T>
Conditional Type
As we've seen above, the ReturnType<T>
lets us extract the return type of a function type.
The definition:
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
Some examples:
type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // boolean
#The Parameters<T>
Conditional Type
The Parameters<T>
type lets us extract all parameter types of a function type. It produces a tuple type with all the parameter types (or the type never
if T
is not a function).
The definition:
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never;
Notice that the Parameters<T>
type is almost identical in structure to the ReturnType<T>
type. The main difference is the placement of the infer
keyword.
Some examples:
type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]
The Array.isArray()
method expects exactly one argument of an arbitrary type; this is why type B
is resolved as [any]
, a tuple with exactly one element. The Math.max()
method, on the other hand, expects arbitrarily many numeric arguments (not a single array argument); therefore, type D
is resolved as number[]
(and not [number[]]
).
#The ConstructorParameters<T>
Conditional Type
The ConstructorParameters<T>
type lets us extract all parameter types of a constructor function type. It produces a tuple type with all the parameter types (or the type never
if T
is not a function).
The definition:
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<
T extends new (...args: any[]) => any
> = T extends new (...args: infer P) => any ? P : never;
Notice that the ConstructorParameters<T>
type is almost identical to the Parameters<T>
type. The only difference is the additional new
keyword that indicates that the function can be constructed.
Some examples:
type A = ConstructorParameters<ErrorConstructor>;
// [(string | undefined)?]
type B = ConstructorParameters<FunctionConstructor>;
// string[]
type C = ConstructorParameters<RegExpConstructor>;
// [string, (string | undefined)?]
#The InstanceType<T>
Conditional Type
The InstanceType<T>
type lets us extract the return type of a constructor function type. It is the equivalent of ReturnType<T>
for constructor functions.
The definition:
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends new (...args: any[]) => any> = T extends new (
...args: any[]
) => infer R
? R
: any;
Once again, notice how the InstanceType<T>
type is very similar in structure to the ReturnType<T>
and ConstructorParameters<T>
types.
Some examples:
type A = InstanceType<ErrorConstructor>; // Error
type B = InstanceType<FunctionConstructor>; // Function
type C = InstanceType<RegExpConstructor>; // RegExp
This article and 44 others are part of the TypeScript Evolution series. Have a look!