Conditional types (T extends U ? X : Y)

Selects between two types based on whether one type is assignable to another.

Signature
SomeType extends OtherType ? TrueType : FalseType
  • Added in TypeScript 2.8

Parameters

ParameterTypeDescription
$SomeTypetypeThe type being tested. A bare type parameter here makes the conditional distributive.
$OtherTypetypeThe type tested against for assignability. May contain infer declarations.
$TrueTypetypeThe result when the check succeeds.
$FalseTypetypeThe result when the check fails.

Return value

TrueType or FalseType, or a deferred conditional type when the check depends on an unresolved type parameterTrueType when SomeType is assignable to OtherType, otherwise FalseType. If SomeType is a naked type parameter and a union is supplied, the conditional distributes over the union members.

Errors

Excessively deep recursion reports "Type instantiation is excessively deep and possibly infinite"; TypeScript caps recursive conditional types at roughly 50 levels of eager instantiation.

Changelog

  • PHP 2.8Introduced, along with infer, distributive behaviour and the Exclude, Extract, NonNullable, ReturnType and InstanceType utility types built on them.
  • PHP 4.1Tail-recursive conditional types are evaluated much more efficiently, making recursive string and tuple manipulation practical.
  • PHP 4.7infer declarations gained extends constraints, so a branch can require the inferred type to match a shape.

A conditional type is an if for the type system. T extends U ? X : Y resolves to X when T is assignable to U and to Y otherwise. Note that extends here means assignable to, not declared as a subclass of.

When to reach for it

The classic case is collapsing overloads. A function whose return type depends on the type of its argument needs three or four overload signatures; one conditional type expresses the same relationship once and keeps working for union arguments, where overload resolution gives up.

The second case is a soft constraint. T extends { message: unknown } ? T["message"] : never lets a type accept anything and quietly produce never for inputs that do not fit, instead of erroring at the declaration site the way a hard T extends ... constraint would.

What trips people up

Conditionals are deferred while the checked type is still an unresolved parameter, so inside a generic function body the compiler cannot prove that a value matches the true branch. Implementations of a function returning a conditional type almost always need an internal cast or an overload signature.

The other surprise is distribution: a bare type parameter on the left silently applies the conditional to each member of a union.

Example

Example · typescript
interface Animal { live(): void }
interface Dog extends Animal { woof(): void }

type Example1 = Dog extends Animal ? number : string;
// type Example1 = number

type Example2 = RegExp extends Animal ? number : string;
// type Example2 = string

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
type EmailBody = MessageOf<{ message: string }>; // string
type DogBody = MessageOf<Dog>;                   // never

type NonNull<T> = T extends null | undefined ? never : T;
type Clean = NonNull<string | null | undefined>; // string

When to use it

  • Give a function one signature whose return type depends on its argument type
  • Write a soft constraint that yields never for inputs that do not match a shape
  • Filter members out of a union, the way Exclude and Extract do

Frequently asked questions

What does extends mean in a conditional type?

It means "is assignable to", not "inherits from". Dog extends Animal is true because a Dog can be used wherever an Animal is expected, and "a" extends string is true for the same reason even though no class hierarchy is involved.

Why does TypeScript say my return value is not assignable to the conditional type?

Because the conditional stays deferred while T is an unresolved type parameter, so inside the function body neither branch has been chosen yet. Add overload signatures for the concrete cases, or cast the return value once at the boundary.

How do I stop a conditional type distributing over a union?

Wrap both sides of extends in a one-element tuple: [T] extends [U] ? X : Y. That makes the left side no longer a naked type parameter, so the union is tested as a single type.

More examples

Replacing a stack of overloads

The overload version needs a third catch-all signature and still cannot express the union case cleanly. The conditional version states the rule once: number in, IdLabel out. The last line is where it wins outright - a union argument distributes through the conditional and produces NameLabel | IdLabel, which overload resolution would have rejected.

Example · typescript
interface IdLabel { id: number }
interface NameLabel { name: string }

// Before: three signatures plus an implementation
function createLabelOld(id: number): IdLabel;
function createLabelOld(name: string): NameLabel;
function createLabelOld(nameOrId: string | number): IdLabel | NameLabel;
function createLabelOld(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

// After: one signature, one conditional type
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

let a = createLabel("typescript"); // NameLabel
let b = createLabel(2.8);          // IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // NameLabel | IdLabel

A type-name dispatcher and chained conditions

Conditionals nest in the false branch exactly like chained ternaries, and the first matching branch wins - which is why the literal type "a" resolves to "string". T11 shows collapsing: both members hit the same branch, so the union of two identical results is a single "object".

Example · typescript
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T0 = TypeName<string>;      // "string"
type T1 = TypeName<"a">;         // "string"
type T2 = TypeName<true>;        // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;    // "object"

// distribution means a union input gives a union output
type T10 = TypeName<string | (() => void)>; // "string" | "function"
type T11 = TypeName<string[] | number[]>;   // "object"

Discussion

  • Be the first to comment on this lesson.