Conditional types in TypeScript

[2025-02-15] dev, typescript
(Ad, please don’t block)

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.

Notation used in this blog post  

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

Syntax and first examples  

A conditional type has the following syntax:

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

A conditional type has three parts:

  • If Sub is assignable to Super... (condition)
  • ...then the result of this type expression is TrueBranch.
  • Otherwise, the result is 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
  >>,
];

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

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 blog post “Computing with tuple types in TypeScript” – from which this example was taken.

Example: checking assignability  

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

Example: only wrapping types that have the property .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 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 } ? [T] : T;
type _ = Assert<Equal<
  WrapLen<boolean | 'hello' | Array<number>>,
  boolean | ['hello'] | [Array<number>]
>>;

In other words, distributivity enables us to “loop” over the elements of a union type.

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 ? [T] : never;
type _ = Assert<Equal<
  Left<'a'|'b'>,
  ['a'] | ['b']
>>;

What about the right-hand side, though? There, no distribution occurs:

type Right<T> = any extends T ? [T] : never;
type _ = Assert<Equal<
  Right<'a'|'b'>,
  ['a' | 'b']
>>;

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

Preventing distributivity  

Consider the following generic type:

type IsString1<T> = T extends string ? 'yes' : 'no';
type _ = [
  Assert<Equal<
    IsString1<string>, 'yes'
  >>,
  Assert<Equal<
    IsString1<number>, 'no'
  >>,
  Assert<Equal<
    IsString1<string | number>, 'yes' | 'no' // (A)
  >>,
];

In line A, we can see that IsString1 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 it to tell us that the complete type string|number is not assignable to string. This is how we can prevent distributivity:

type IsString2<T> = [T] extends [string] ? 'yes' : 'no';
type _ = [
  Assert<Equal<
    IsString2<string>, 'yes'
  >>,
  Assert<Equal<
    IsString2<number>, 'no'
  >>,
  Assert<Equal<
    IsString2<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.

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

Filtering union types  

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

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.

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.

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 the blog post “TypeScript: extracting parts of composite types via infer.

Deferred conditional types  

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)
  >>;
  // ···
}

In line A, we can see that Result is neither string nor number, but in a deferred state.

Sources of this blog post