In TypeScript, conditional types let us make decisions (think if-then-else expressions) – which is especially useful in generic types. They are also an essential tool for working with union types because they let use “loop” over them. Read on if you want to know how all of that works.
For showing computed and inferred types in source code, I use the npm package asserttt
– e.g.:
// Types of values
assertType<string>('abc');
assertType<number>(123);
// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
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 blog post “Computing with tuple types in TypeScript” – from which this example was taken.
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
>>,
];
.length
In the following example, Wrap<>
only wraps types in one-element tuples if they have the property .length
whose values are numbers:
type WrapLen<T> = T extends { length: number } ? [T] : T;
type _ = [
Assert<Equal<
WrapLen<string>,
[string]
>>,
Assert<Equal<
WrapLen<RegExp>,
RegExp
>>,
];
Conditional types are distributive: Applying a conditional type C
to a union type U
is the same as the union of applying C
to each component of U
. This is an example:
type WrapLen<T> = T extends { length: number } ? [T] : T;
type _ = Assert<Equal<
WrapLen<boolean | 'hello' | Array<number>>,
boolean | ['hello'] | [Array<number>]
>>;
In other words, distributivity enables us to “loop” over the components of a union type.
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<number | 'a' | 'b'>,
[number] | ['a'] | ['b']
>>;
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<number | 'a' | 'b'>,
[number | 'a' | 'b']
>>;
never
to ignore things 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
.
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 types infer
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 the blog post “TypeScript: extracting parts of composite types via infer
”.
A conditional type is deferred if its condition contains type variables that don’t have a value yet – 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)
>>;
// ···
}
The generic helper type IsAssignableFrom
was defined earlier.
In line A, we can see that Result
is neither string
nor number
, but in a deferred state.