Warning: This page is a work in progress

Conditional Types

At the heart of most useful programs, we have to make decisions based on input. JavaScript programs are no different, but given the fact that values can be easily introspected, those decisions are also based on the types of the inputs. Conditional types help describe the relation between the types of inputs and outputs.

interface Animal { live(): void; } interface Dog extends Animal { woof(): void; } type Foo = Dog extends Animal ? number : string; // ^ = type Foo = number type Bar = RegExp extends Animal ? number : string; // ^ = type Bar = stringTry

Conditional types take a form that looks a little like conditional expresions (cond ? trueExpression : falseExpression) in JavaScript:

SomeType extends OtherType ? TrueType : FalseType;Try

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

From the examples above, conditional types might not immediately seem useful - we can tell ourselves whether or not Dog extends Animal and pick number or string! But the power of conditional types comes from using them with generics.

For example, let’s take the following createLabel function:

interface IdLabel { id: number /* some fields */; } interface NameLabel { name: string /* other fields */; } function createLabel(id: number): IdLabel; function createLabel(name: string): NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel { throw "unimplemented"; }Try

These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:

  1. If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.
  2. We have to create three overloads: one for each case when we’re sure of the type (one for string and one for number), and one for the most general case (taking a string | number). For every new type createLabel can handle, the number of overloads grows exponentially.

Instead, we can encode that logic in a conditional type:

type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;Try

We can then use that conditional type to simplify out overloads down to a single function with no overloads.

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { throw "unimplemented"; } let a = createLabel("typescript"); // ^ = let a: NameLabel let b = createLabel(2.8); // ^ = let b: IdLabel let c = createLabel(Math.random() ? "hello" : 42); // ^ = let c: NameLabel | IdLabelTry

Conditional Type Constraints

Often, the checks in a conditional type will provide us with some new information. Just like with narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constraint generics by the type we check against.

For example, let’s take the following:

type MessageOf<T> = T["message"]; Type '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.Try

In this example, TypeScript errors because T isn’t known to have a property called message. We could constrain T, and TypeScript would no longer complain:

type MessageOf<T extends { message: unknown }> = T["message"]; interface Email { message: string; } interface Dog { bark(): void; } type EmailMessageContents = MessageOf<Email>; // ^ = type EmailMessageContents = stringTry

However, what if we wanted MessageOf to take any type, and default to something like never if a message property isn’t available? We can do this by moving the constraint out and introducing a conditional type:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; interface Email { message: string; } interface Dog { bark(): void; } type EmailMessageContents = MessageOf<Email>; // ^ = type EmailMessageContents = string type DogMessageContents = MessageOf<Dog>; // ^ = type DogMessageContents = neverTry

Within the true branch, TypeScript knows that T will have a message property.

As another example, we could also write a type called Flatten that flattens array types to their element types, but leaves them alone otherwise:

type Flatten<T> = T extends any[] ? T[number] : T; // Extracts out the element type. type Str = Flatten<string[]>; // ^ = type Str = string // Leaves the type alone. type Num = Flatten<number>; // ^ = type Num = numberTry

When Flatten is given an array type, it uses an indexed access with number to fetch out string[]’s element type. Otherwise, it just returns the type it was given.

Inferring Within Conditional Types

We just found ourselves using conditional types to apply constraints and then extract out types. This ends up being such a common operation that conditional types make it easier.

Conditional types provide us with a way to infer from types we compare against in the true branch using the infer keyword. For example, we could have inferred the element type in Flatten instead of fetching it out “manually” with an indexed access type:

type Flatten<T> = T extends Array<infer U> ? U : T;Try

Here, we used the infer keyword declaratively introduced a new generic type variable named U instead of specifying how to retrieve the element type of T. Within the true branch This frees us from having to think about how to dig through and probing apart the structure of the types we’re interested.

We can write some useful helper type aliases using the infer keyword. For example, for simple cases, we can extract the return type out from function types:

type GetReturnType<T> = T extends (...args: never[]) => infer U ? U : never; type Foo = GetReturnType<() => number>; // ^ = type Foo = number type Bar = GetReturnType<(x: string) => string>; // ^ = type Bar = string type Baz = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // ^ = type Baz = boolean[]Try

Distributive Conditional Types

When conditional types act on a generic type, they become distributive when given a union type. For example, take the following:

type Foo<T> = T extends any ? T[] : never;Try

If we plug a union type into Foo, then the conditional type will be applied to each member of that union.

type Foo<T> = T extends any ? T[] : never; type Bar = Foo<string | number>; // ^ = type Bar = string[] | number[]Try

What happens here is that Foo distributes on

string | number;Try

and maps over each member type of the union, to what is effectively

Foo<string> | Foo<number>;Try

which leaves us with

string[] | number[];Try

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

type Foo<T> = [T] extends [any] ? T[] : never; // 'Bar' is no longer a union. type Bar = Foo<string | number>; // ^ = type Bar = (string | number)[]Try

The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request

Contributors to this page:
OTOrta Therox  (3)

Last updated: Nov 23, 2020