HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

34 Conditional types (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.

34.1 Syntax and first examples

A conditional type has the following syntax:

«Sub» extends «Super» ? «TrueBranch» : «FalseBranch»

A conditional type has three parts:

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
  >>,
];

34.1.1 Chaining conditional types

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'
  >>,
];

34.1.2 Nesting conditional types

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.

34.1.3 Example: only wrapping types that have the property .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
  >>,
];

34.2 Conditional types are distributive over union types

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
  >>,
];

34.2.1 Only the left-hand side of extends is distributed

We 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'>
>>;

34.2.2 Only type variables trigger distribution

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'
>>;

34.2.3 Preventing distributivity

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.

34.2.4 Technique: always applying

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]
>>;

34.3 Filtering union types by conditionally returning 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
>>;

34.3.1 The built-in utility type 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).

34.3.2 The built-in utility type 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.

34.4 Extracting parts of composite types via 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 “Extracting parts of compound types via infer” (§35).

34.5 Writing conditions for conditional types

34.5.1 Is one type assignable to another one?

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.

34.5.2 Checking if a generic type returns a particular value

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).

34.5.3 Checking if two types are equal

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.

34.5.4 Logical Or

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
  >>,
];

34.5.5 Logical And

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).

34.6 Deferred conditional types

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.

34.7 Sources of this chapter