Conditional types in TypeScript

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

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.

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

Mapping over union types  

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.

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  

With distributive conditional types, we use type 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
>>;

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

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.

Sources of this blog post