Distributive conditionals
A naked type parameter in a conditional distributes over each member of a union; wrapping it in a tuple opts out — and a conditional over never yields never because never is the empty union.
A teammate writes type NonEmpty<T> = T extends "" ? never : T to strip empty strings, tests it on "a" | "", gets "a", ships it. Two weeks later a different call site passes never (the type of an exhausted union) and NonEmpty<never> quietly returns never — which is correct, but for a reason nobody on the team can explain, and now a generic helper silently produces never where they expected the input back. The behaviour is not a bug. It is distribution, and never is the empty union. Once you can read distribution precisely, half of the standard utility types stop being magic.
The rule, stated precisely
Why does this matter? Because every union-level utility type — Exclude, Extract, Filter — works because of this rule, and every wrong IsNever or IsString predicate breaks because of it. When the type being checked in a conditional is a naked type parameter — T extends ..., where T appears alone, not wrapped in anything — and T is instantiated with a union, the conditional distributes: it is applied to each member of the union independently, and the results are unioned back together.
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// ^? type A = string[] | number[]
// NOT (string | number)[] — distribution split the union firstThe compiler rewrites ToArray<string | number> as ToArray<string> | ToArray<number> = string[] | number[]. The check runs once per member.
Opting out: wrap the parameter
Distribution only triggers for a naked parameter. Wrap it in a one-element tuple and the union is checked as a single whole:
type IsString<T> = [T] extends [string] ? true : false;
type B = IsString<string | number>;
// ^? type B = false ("string | number" is not assignable to string — checked as a whole)
// Without the wrap, distribution gives a surprising union:
type IsStringNaked<T> = T extends string ? true : false;
type C = IsStringNaked<string | number>;
// ^? type C = boolean (true | false — distributed: string->true, number->false)Notice C is boolean. That is true | false — distribution produced true for the string member and false for the number member, then unioned them. This is the single most common distribution surprise: a predicate that should answer “yes or no about the whole type” instead answers per-member and collapses to boolean.
never is the empty union
never is the type with zero members — the empty union. Distributing a conditional over zero members produces zero results, and the union of nothing is never:
type Wrap<T> = T extends unknown ? T[] : never;
type D = Wrap<never>;
// ^? type D = never (NOT never[] — there were no members to map over)This is the famous “conditional over never yields never” gotcha. Wrap<never> is not never[]; the conditional never runs at all because there is nothing to distribute over. To force evaluation when you genuinely want to test for never, opt out of distribution:
type IsNever<T> = [T] extends [never] ? true : false;
type E = IsNever<never>;
// ^? type E = true (wrapped: the empty union IS assignable to [never])▸Why this works
Why does boolean also distribute? boolean is internally true | false — a union of two literal types. So T extends true ? ... : ... over T = boolean runs twice, once for true and once for false. If you see a conditional mysteriously returning boolean or evaluating both arms when you passed a single boolean, this is why. Wrap with [boolean] extends [true] to treat it atomically.
Rebuilding the standard library with distribution
Exclude and Extract are pure distribution — three tokens each:
type MyExclude<T, U> = T extends U ? never : T; // drop members of T assignable to U
type MyExtract<T, U> = T extends U ? T : never; // keep members of T assignable to U
type MyNonNullable<T> = T extends null | undefined ? never : T;
type F = MyExclude<"a" | "b" | "c", "a">;
// ^? type F = "b" | "c"
// distributed: "a"->never, "b"->"b", "c"->"c"; never drops out of the unionThe key insight: distribution maps each member to either itself or never, and never disappears when unioned (X | never = X). That is the whole mechanism behind filtering a union — map the unwanted members to never and they vanish. You can build a general filter the same way:
type FilterByKind<T, K> = T extends { kind: K } ? T : never;
type Events = { kind: "click"; x: number } | { kind: "key"; code: string };
type G = FilterByKind<Events, "click">;
// ^? type G = { kind: "click"; x: number }Given `type P<T> = T extends any ? T[] : never`, what does `type R = P<never>` resolve to, and why?
A distributive conditional is like a mail sorter: each letter (union member) is processed one at a time and dropped into an outbox. To stop the sorter from opening the envelope and processing each letter separately — to handle the whole bundle as one — you ____ the type parameter.
- 01State the precise condition under which a conditional type distributes, and give the two-token change that opts out.
- 02Why does a distributive conditional over `never` return `never` rather than the mapped result, and how do you test for `never` reliably?
- 03How are Exclude, Extract, and NonNullable just distribution, and what role does `never` play in the result?
Distribution is the single rule that makes union-level type programming work: a naked type parameter checked against a union runs the conditional once per member and unions the outcomes. Wrapping in [T] evaluates the union as a whole — essential for IsNever, IsString-style predicates, and anything that should answer about the whole type rather than each member. never is the empty union, so distributing over it yields never; boolean is true | false and distributes into both arms. Exclude/Extract/NonNullable are distribution mapping members to never, which vanishes on recombination. Now when you see a generic predicate collapse to boolean or a filtered type return never where you expected a value, the first thing you check is whether the parameter is naked and whether it might be never.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.