Conditional Types
An if-statement for types
Section titled “An if-statement for 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.
Nesting: a small decision tree
Section titled “Nesting: a small decision tree”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.
Distribution over unions
Section titled “Distribution over unions”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 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.
Turning distribution off
Section titled “Turning distribution off”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).