Skip to content

Conditional Types

A conditional type chooses one of two types based on a relationship, using the ternary syntax you already know — but at the type level:

type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hi">; // "yes" ("hi" extends string)

Read T extends U ? X : Y as “if T is assignable to U, the type is X, otherwise Y.” The extends here is the same subset test from the foundations module — not inheritance.

Conditionals nest to form multi-way branches:

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 T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<number[]>; // "object"

Each condition is tried in order, exactly like a chain of else if.

Here is the behavior that surprises everyone. When the checked type is a naked type parameter and you hand it a union, the conditional distributes over each member and unions the results:

type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;
// ^? string[] | number[]

It did not produce (string | number)[]. Instead the conditional ran separately for string (giving string[]) and for number (giving number[]), then unioned them. This is distributive conditional type behavior.

flowchart TB
  input["ToArray of string or number"] --> split["distribute over each member"]
  split --> s["string branch: string array"]
  split --> n["number branch: number array"]
  s --> out["union the results:
string array or number array"]
  n --> out
A conditional distributes over each union member

A practical use is filtering a union. Exclude (a built-in) is exactly this:

type MyExclude<T, U> = T extends U ? never : T;
type Colors = "red" | "green" | "blue";
type NoRed = MyExclude<Colors, "red">;
// ^? "green" | "blue"

Each member that matches U becomes never (and never vanishes from a union), so only the non-matching members survive.

Sometimes you want to test the union as a whole, not member by member. Wrap both sides in a one-element tuple to stop distribution — the type parameter is no longer “naked”:

type IsUnionOfStrings<T> = [T] extends [string] ? true : false;
type D1 = IsUnionOfStrings<"a" | "b">; // true (tested as a whole)
// compare with the distributive version:
type Bad<T> = T extends string ? true : false;
type D2 = Bad<"a" | number>; // boolean (true | false — distributed!)
type D3 = IsUnionOfStrings<"a" | number>; // false (whole union isn't all string)

The [T] extends [U] trick is a standard idiom: reach for it whenever a conditional over a union gives you a surprising boolean (a sign it distributed and unioned true | false).

How do you read `T extends U ? X : Y`?
What is `ToArray<string | number>` for `type ToArray<T> = T extends any ? T[] : never`?
How does the built-in `Exclude<T, U>` remove members from a union?
Why wrap both sides as `[T] extends [U]`?