C ? T : F
)A conditional type in TypeScript is an if-then-else expression: Its result is either one of two branches – which one depends on a condition. That is especially useful in generic types. Conditional types are also an essential tool for working with union types because they let us “loop” over them. Read on if you want to know how all of that works.
A conditional type has the following syntax:
«Sub» extends «Super» ? «TrueBranch» : «FalseBranch»
A conditional type has three parts:
Sub
is assignable to Super
… (condition)
TrueBranch
.
FalseBranch
.
I like to format longer conditional types like this:
«Sub» extends «Super»
? «TrueBranch»
: «FalseBranch»
This is a first example of using conditional types:
type IsNumber<T> = T extends number ? true : false;
type _ = [
Assert<Equal<
IsNumber<123>, true
>>,
Assert<Equal<
IsNumber<number>, true
>>,
Assert<Equal<
IsNumber<'abc'>, false
>>,
];
Similarly to JavaScript’s ternary operator, we can also chain TypeScript’s conditional type operator:
type PrimitiveTypeName<T> =
T extends undefined ? 'undefined' :
T extends null ? 'null' :
T extends boolean ? 'boolean' :
T extends number ? 'number' :
T extends bigint ? 'bigint' :
T extends string ? 'string' :
never;
type _ = [
Assert<Equal<
PrimitiveTypeName<123n>,
'bigint'
>>,
Assert<Equal<
PrimitiveTypeName<bigint>,
'bigint'
>>,
];
In the previous example, the true branch was always short and the false branch contained the next (nested) conditional type. That’s why each conditional type has the same indentation.
However, if a nested conditional type appears in a true branch, then indentation helps humans read the code – e.g.:
type RemoveEmptyStrings<T extends Array<string>> =
T extends [
infer First extends string,
...infer Rest extends Array<string>
]
? First extends ''
? RemoveEmptyStrings<Rest>
: [First, ...RemoveEmptyStrings<Rest>]
: []
;
type _ = Assert<Equal<
RemoveEmptyStrings<['', 'a', '', 'b', '']>,
["a", "b"]
>>;
For more information on this code, see the “Filtering a tuple” (§37.6.4.2) – from which this example was taken.
.length
In the following example, Wrap<>
only wraps types in Promises if they have the property .length
whose values are numbers:
type WrapLen<T> = T extends { length: number } ? Promise<T> : T;
type _ = [
Assert<Equal<
WrapLen<string>,
Promise<string>
>>,
Assert<Equal<
WrapLen<boolean>,
boolean
>>,
];
Conditional types are distributive over union types: Applying a conditional type C
to a union type U
is the same as the union of applying C
to each element of U
. This is an example:
type WrapLen<T> = T extends { length: number } ? Promise<T> : T;
type _1 = Assert<Equal<
WrapLen<'hello' | boolean | Array<number>>, // (A)
Promise<'hello'> | boolean | Promise<Array<number>>
>>;
Distributivity enables us to “loop” over the elements of the union type in line A: WrapLen<T>
is applied to each element and only wraps values that have a property .length
whose value is a number.
For comparison, this is what happens with non-union types:
type _2 = [
Assert<Equal<
WrapLen<string>,
Promise<string>
>>,
Assert<Equal<
WrapLen<boolean>,
boolean
>>,
];
extends
is distributedWe have already seen that conditional types are distributed over the left-hand side of extends
:
type Left<T> = T extends any ? Promise<T> : never;
type _ = Assert<Equal<
Left<'a'|'b'>,
Promise<'a'> | Promise<'b'>
>>;
What about the right-hand side, though? There, no distribution occurs:
type Right<T> = any extends T ? Promise<T> : never;
type _ = Assert<Equal<
Right<'a'|'b'>,
Promise<'a' | 'b'>
>>;
If we directly mention a union type in the condition of a conditional type then no distribution happens:
type IsTrue = false | true extends true ? 'yes' : 'no';
type _ = Assert<Equal<
IsTrue,
'no'
>>;
Compare that with using the type variable T
:
type IsTrue<T> = T extends true ? 'yes' : 'no';
type _ = Assert<Equal<
IsTrue<false | true>,
'no' | 'yes'
>>;
Consider the following generic type:
type IsString<T> = T extends string ? 'yes' : 'no';
type _ = [
Assert<Equal<
IsString<string>, 'yes'
>>,
Assert<Equal<
IsString<number>, 'no'
>>,
Assert<Equal<
IsString<string | number>, 'yes' | 'no' // (A)
>>,
];
In line A, we can see that IsString
is distributive – which makes sense since we have used a conditional type to define it. But that is not what we want in this case: We’d like IsString
to tell us that the complete type string|number
is not assignable to string
. This is how we can prevent distributivity:
type IsString<T> = [T] extends [string] ? 'yes' : 'no';
type _ = [
Assert<Equal<
IsString<string>, 'yes'
>>,
Assert<Equal<
IsString<number>, 'no'
>>,
Assert<Equal<
IsString<string | number>, 'no'
>>,
];
A conditional type is only distributive if the left-hand side of extends
is a bare type variable. By wrapping both the left-hand side and the right-hand side of extends
, the intended check still happens but there is no distribution.
Conditional types are an important tool for working with union types because they enable us to loop over them. Sometimes, we simply want to unconditionally map each union element to a new type. Then we can use the following technique:
type AlwaysWrap<T> = T extends any ? [T] : never;
type _ = Assert<Equal<
AlwaysWrap<boolean>,
[false] | [true]
>>;
Note how type boolean
really is just the union false | true
.
The following (seemingly simpler) approach does not work – T
needs to be part of the condition. Otherwise, the conditional type is not distributive.
type AlwaysWrap<T> = true extends true ? [T] : never;
type _ = Assert<Equal<
AlwaysWrap<boolean>,
[boolean]
>>;
never
Interpreted as a set, type never
is empty. Therefore, if it appears in a union type, it is ignored:
type _ = Assert<Equal<
'a' | 'b' | never,
'a' | 'b'
>>;
That means we can use never
to ignore components of a union type:
type DropNumber<T> = T extends number ? never : T;
type _ = Assert<Equal<
DropNumber<1 | 'a' | 2 | 'b'>,
'a' | 'b'
>>;
This is what happens if we swap the type expressions of the true branch and the false branch:
type KeepNumber<T> = T extends number ? T : never;
type _ = Assert<Equal<
KeepNumber<1 | 'a' | 2 | 'b'>,
1 | 2
>>;
Exclude<T, U>
Excluding types from a union is such a common operation that TypeScript provides the built-in utility type Exclude<T, U>
:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
type Union = 1 | 'a' | 2 | 'b';
type _ = [
Assert<Equal<
Exclude<Union, number>,
'a' | 'b'
>>,
Assert<Equal<
Exclude<Union, 1 | 'a' | 'x'>,
2 | 'b'
>>,
];
Interpreted as a set operation, Exclude<T, U>
is T − U
.
To see an interesting use case for Exclude
, check out “Extracting a subtype of a discriminated union” (§19.2.3).
Extract<T, U>
The inverse of Exclude<T, U>
is Extract<T, U>
(which is also built into TypeScript):
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
type Union = 1 | 'a' | 2 | 'b';
type _ = [
Assert<Equal<
Extract<Union, number>,
1 | 2
>>,
Assert<Equal<
Extract<Union, 1 | 'a' | 'x'>,
1 | 'a'
>>,
];
Interpreted as a set operation, Extract<T, U>
is T ∩ U
.
infer
in conditional typesinfer
lets us extract parts of compound types and can only be used inside the extends
clause of a conditional type:
type ElemType<Arr> = Arr extends Array<infer Elem> ? Elem : never;
type _ = Assert<Equal<
ElemType<Array<string>>, string
>>;
For more information, see “Extracting parts of compound types via infer
” (§35).
We can use a conditional type to implement an assignability check:
type IsAssignableFrom<A, B> = B extends A ? true : false;
type _ = [
Assert<Equal<
// Type `123` is assignable to type `number`
IsAssignableFrom<number, 123>,
true
>>,
Assert<Equal<
// Type `'abc'` is not assignable to type `number`
IsAssignableFrom<number, 'abc'>,
false
>>,
];
Why is this correct? Recall that, in the condition of a conditional type, A extends B
checks if A
is assignable to B
.
In the following code, we check if Str
is equal to Uppercase<Str>
:
type IsUppercase<Str extends string> = Str extends Uppercase<Str>
? true
: false;
type _ = [
Assert<Equal<
IsUppercase<'SUNSHINE'>, true
>>,
Assert<Equal<
IsUppercase<'SUNSHINe'>, false
>>,
];
We don’t really check equality, we only check if Str
is assignable to Uppercase<Str>
– via extends
.
There is one thing to watch out for – never
is assignable to all types:
type _ = [
Assert<Equal<
never extends true ? 'yes' : 'no',
'yes'
>>,
Assert<Equal<
never extends false ? 'yes' : 'no',
'yes'
>>,
];
For more information on Uppercase
, see “Utility types for string manipulation” (§38.3).
In the next example, we use the generic utility type Equal
from asserttt
to check if two types are equal (line A):
type SimplifyTuple<Tup> =
Tup extends [infer A, infer B]
? (Equal<A, B> extends true ? [A] : [A, B]) // (A)
: never
;
type _ = [
Assert<Equal<
SimplifyTuple<['a', 'b']>,
['a', 'b']
>>,
Assert<Equal<
SimplifyTuple<['a', 'a']>,
['a']
>>,
];
Note that we check if the result of Equal<A, B>
is assignable to true
.
To check if X || Y
, we check:
X ? true
: Y ? true
: false
Example:
type ContainsNumber<Tup> =
Tup extends [infer A, infer B]
? A extends number ? true
: B extends number ? true
: false
: never
;
type _ = [
Assert<Equal<
ContainsNumber<['a', 'b']>,
false
>>,
Assert<Equal<
ContainsNumber<[1, 'a']>,
true
>>,
Assert<Equal<
ContainsNumber<[number, 'a']>,
true
>>,
];
To check if X && Y
, we can use a trick and check [X, Y]
via extends
– e.g.:
export type TEqual<X, Y> =
[IsAny<X>, IsAny<Y>] extends [true, true] ? true
: [IsAny<X>, IsAny<Y>] extends [false, false] ? MutuallyAssignable<X, Y>
: false
For more information on this code, see “Ensuring the any
is only equal to itself” (§39.3.3).
To conclude, let’s look at an interesting phenomenon: Normally, the result of a conditional type is either its true branch or its false branch. However, if its condition contains one or more type variables that don’t have a value yet then it is deferred and not turned into a simpler value – e.g.:
type StringOrNumber<Kind extends 'string' | 'number'> =
Kind extends 'string' ? string : number
;
function randomValue<K extends 'string' | 'number'>(kind: K) {
type Result = StringOrNumber<K>;
type _ = Assert<Equal<
Result,
K extends 'string' ? string : number // (A)
>>;
// ···
}
In line A, we can see that Result
is neither string
nor number
, but a deferred conditional type.