Distributive Conditional Types
When a conditional type checks a naked type parameter and that parameter is a union, TypeScript applies the condition to each member of the union separately and then unions the results. This behavior is called distribution, and it is what makes utility types like Exclude and NonNullable filter unions member by member. Distribution is powerful but surprising — it changes the result compared to checking the union as a whole, and it has special behavior for never. This page explains when distribution kicks in, how to turn it off, and the consequences for the empty type.
What distribution means
A conditional type distributes only when the checked type is a bare type parameter (T extends ..., not [T] extends ... or Foo<T> extends ...). Given a union, the conditional is applied independently to each constituent.
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// distributes to: ToArray<string> | ToArray<number>
// => string[] | number[] (NOT (string | number)[])
Because T is naked, ToArray<string | number> becomes ToArray<string> | ToArray<number>, yielding string[] | number[]. If distribution did not occur you would get (string | number)[], which is a different type.
Filtering unions
Distribution is exactly what enables filtering: map each member to either itself or never, and the never members vanish from the resulting union.
type MyExclude<T, U> = T extends U ? never : T;
type Status = "loading" | "success" | "error";
type NonError = MyExclude<Status, "error">; // "loading" | "success"
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Clean = MyNonNullable<string | null | undefined>; // string
Each member of Status is tested against "error": the matching member resolves to never and drops out, leaving the rest. This is precisely how the built-in Exclude and NonNullable work.
The role of never
never is the empty union — a union with zero members. Distributing over an empty union produces never, because there are no members to map. This is a frequent gotcha.
type Wrap<T> = T extends any ? T[] : never;
type A = Wrap<never>; // never (no members to distribute over!)
type B = Wrap<string>; // string[]
Wrap<never> is never, not never[], because the distribution has nothing to iterate. If you actually want to treat never as a single value, you must disable distribution (shown next).
Disabling distribution
Wrap both sides of the extends in a one-element tuple. This makes the checked type no longer naked, so the union is evaluated as a whole.
type IsNever<T> = [T] extends [never] ? true : false;
type A = IsNever<never>; // true
type B = IsNever<string>; // false
// Non-distributive ToArray
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type R = ToArrayND<string | number>; // (string | number)[]
IsNever is the canonical way to detect never, which a plain T extends never ? ... cannot do (it would distribute to never). Tuple-wrapping in ToArrayND keeps the union intact, producing (string | number)[].
| Form | Distributes? | ToArray<string | number> |
|---|---|---|
T extends U ? ... : ... | Yes (T is naked) | string[] | number[] |
[T] extends [U] ? ... : ... | No | (string | number)[] |
Foo<T> extends U ? ... | No (T not naked) | as a whole |
Practical implications
Knowing when distribution applies prevents subtle bugs. Use it intentionally to transform each union member, and suppress it when you need the union compared atomically — such as equality checks or guarding against never.
// Distributive: good for per-member transforms
type Nullable<T> = T extends any ? T | null : never;
type N = Nullable<"a" | "b">; // "a" | null | "b" | null => "a" | "b" | null
// Non-distributive: good for whole-union comparisons
type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false;
type E1 = Equals<string, string>; // true
type E2 = Equals<string, number>; // false
Nullable benefits from distribution to add null to each member. The Equals helper deliberately avoids naked distribution by comparing function types, giving a reliable strict equality check between two types.
Only the type directly to the left of
extendstriggers distribution, and only when it is an unmodified generic parameter. Any wrapping — tuples,keyof, indexing — switches distribution off.
Best Practices
- Lean on distribution to filter or transform unions member by member.
- Use
[T] extends [U]to disable distribution for whole-union comparisons. - Detect the empty type with
[T] extends [never] ? ..., neverT extends never. - Remember a distributive conditional over
neveralways resolves tonever. - Confirm the checked type is a naked parameter when you expect distribution.
- Reach for built-in
Exclude,Extract, andNonNullablebefore reimplementing them. - Document non-distributive helpers, since the tuple trick is easy to misread.