A Naked `T extends U` Distributes Over Unions
Distributive Conditionals
When the checked type is a naked generic parameter, conditional types distribute over unions. This is huge and surprising.
What you'll learn
- Recognize when distribution happens
- Disable it with `[T] extends [U]`
- Use it to filter unions
When a conditional type checks a naked generic parameter against a type, and that parameter is a union, the conditional distributes over each member of the union.
The Behavior
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
// (NOT (string | number)[]) The conditional ran once per union member, then unioned the results.
Filtering A Union
type StringsOnly<T> = T extends string ? T : never;
type A = StringsOnly<"a" | "b" | 1 | 2>;
// "a" | "b" Distribution + never-as-filter is how Extract<T, U> and
Exclude<T, U> work.
Disabling Distribution
Wrap the operands in tuples to STOP distribution:
type ToArray<T> = [T] extends [any] ? T[] : never;
type A = ToArray<string | number>;
// (string | number)[]
// (no distribution this time) [T] extends [any] checks the tuple type — which isn’t a naked
generic — so distribution doesn’t apply.
When Does Distribution Help
- Filtering unions (
Extract,Exclude) - Mapping over union members (
ToArray<T>) - Building a union of results from a union of inputs
When Does It Hurt
When you want the WHOLE input type, not per-member behavior. Wrap in tuples to disable.
A Common Trap
type AllStrings<T> = T extends string ? "yes" : "no";
type A = AllStrings<"a" | "b">; // "yes" (each member is a string)
type B = AllStrings<"a" | 1>; // "yes" | "no" (mixed!) If you wanted “all of T’s members must be strings”, distribution gives you the wrong answer:
type AllStrings<T> = [T] extends [string] ? "yes" : "no";
type A = AllStrings<"a" | "b">; // "yes"
type B = AllStrings<"a" | 1>; // "no" ✓ Wrap in tuples to check the whole thing.
Up Next
The utility types — many built from these primitives.
Utility Types →