Overview
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:
|
|
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:
|
|
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:
|
|
Let’s now apply the NonNullable<T>
type to EmailAddress
and resolve the resulting type step by step:
|
|
We’ll start by replacing EmailAddress
by the union type that it aliases:
|
|
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:
|
|
We can now replace NonNullable<T>
by its definition everywhere:
|
|
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
:
|
|
Because never
is a subtype of every type, we can omit it from the union type. This leaves us with the final result:
|
|
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:
|
|
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:
|
|
Here’s how we can resolve NonNullablePropertyKeys<User>
. First, we’ll supply the User
type as a type argument for the T
type parameter:
|
|
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:
|
|
Next, we’ll unroll { 展开 } the P in …
mapping and substitute "name"
and "email"
for the P
type:
|
|
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
:
|
|
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:
|
|
We’re now done with both the mapped type and the conditional type. Once more, we’ll resolve keyof User
:
|
|
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:
|
|
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
:
|
|
And just like before, we can simplify the resulting union type by purging { 清洗;消除 } the never
type:
|
|
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:
|
|
We can combine our NonNullablePropertyKeys<T>
type with Pick<T, K>
to define NonNullableProperties<T>
, which is the type that we were looking for:
|
|
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:
|
|
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:
|
|
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:
|
|
Some examples:
|
|
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:
|
|
Some examples:
|
|
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:
|
|
Some examples:
|
|
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:
|
|
Some examples:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
Once again, notice how the InstanceType<T>
type is very similar in structure to the ReturnType<T>
and ConstructorParameters<T>
types.
Some examples:
|
|