why Exclude<A | B, A> works and when distribution gets in the way
When you apply a conditional type to a union type, the condition is by default applied to every member of the union individually, not to the union as a whole. This behavior is called distribution and is the quiet foundation of utility types like Exclude and Extract. Without knowing the naked type parameter rule, you will stumble on unexpected results as soon as a custom utility type deliberately needs to prevent distribution.
Table of Contents
- 1. What distributive conditional types are
- 2. The rule: a naked type parameter is required for distribution
- 3. Practical example: the mechanics of Exclude and Extract in detail
- 4. Preventing distribution with tuple wrapping
- 5. Side effect: distribution over the union never
- 6. Practical example: custom utility types with deliberate control
- 7. Interaction with generic constraints and overloads
- 8. Debugging: when distribution strikes unexpectedly
- 9. Distributive vs. non-distributive compared
- 10. Summary
- 11. FAQ
1. What distributive conditional types are
A conditional type of the form T extends U ? X : Y behaves differently once T is a union type at instantiation time. Instead of checking the entire union against U in a single step, TypeScript applies the condition to every individual member of the union separately and then reassembles the results into a new union. This behavior is called distribution, because the conditional type distributes itself over the union instead of treating it as a single, indivisible type.
Distributive conditional types are not a special case, they are the default behavior of every conditional type, provided a specific syntactic condition is met, explained in the next section. This behavior is the quiet, often unnoticed foundation of many built in utility types, first and foremost Exclude<T, U> and Extract<T, U>, which simply would not work without distribution.
This article explains the exact rule for when distribution occurs, shows the mechanics of Exclude and Extract in detail, demonstrates how to deliberately prevent distribution when it is unwanted, and names the best known side effect involving the type never.
2. The rule: a naked type parameter is required for distribution
Distribution only occurs when the checked type parameter appears in the conditional type as a so-called naked type parameter, meaning unwrapped and direct, without being embedded in an array, tuple, object, or other structure. T extends U ? X : Y distributes because T stands directly and unwrapped in the condition. [T] extends [U] ? X : Y, on the other hand, does not distribute because T is wrapped inside a tuple here.
This rule applies regardless of whether T actually is a union or not. The compiler only checks the syntactic form of the definition, not the concrete instantiation. A conditional type with a naked type parameter therefore always distributes over unions once instantiated with a union, and a conditional type with a wrapped type parameter never does, even when a union is passed in as well.
// Naked type parameter: distributes over union members automatically
type ToArrayNaked<T> = T extends unknown ? T[] : never;
type A = ToArrayNaked<string | number>;
// string[] | number[] — distribution happened
// Wrapped type parameter: does NOT distribute, union is treated as one type
type ToArrayWrapped<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayWrapped<string | number>;
// (string | number)[] — no distribution, union stays intact
3. Practical example: the mechanics of Exclude and Extract in detail
The built in utility types Exclude<T, U> and Extract<T, U> are the clearest example of distributive conditional types in the TypeScript standard library. Exclude<T, U> is defined as T extends U ? never : T, with a naked T as the checked parameter. When T is instantiated with a union, TypeScript checks every member individually: if a member matches U, it is replaced by never, which effectively disappears in a union.
Extract<T, U> works as a mirror image with T extends U ? T : never: members that match U are kept, all others become never and disappear. Without distribution, both utility types would have to check the union as a whole against U, which for Exclude<"a" | "b" | "c", "a"> would either discard the entire union or keep it entirely, instead of selectively removing individual members.
type Exclude2<T, U> = T extends U ? never : T; // naked T -> distributes
type Status = "pending" | "active" | "archived" | "deleted";
// Distribution checks each member of Status individually against "archived" | "deleted"
type ActiveStatus = Exclude2<Status, "archived" | "deleted">;
// "pending" | "active"
type Extract2<T, U> = T extends U ? T : never;
type ArchivedOrDeleted = Extract2<Status, "archived" | "deleted">;
// "archived" | "deleted"
4. Preventing distribution with tuple wrapping
Sometimes distribution is unwanted, for example when a conditional type should check whether a union as a whole matches a certain type, instead of treating each member individually. A typical example is a type that checks whether T is exactly string | number. With a naked type parameter, distribution would compute the result for every combination of members separately, which is not the intended behavior here.
The tuple wrapping pattern [T] extends [U] ? X : Y is the standard solution for deliberately suppressing distribution. Because T is wrapped inside a tuple here, the naked type parameter rule from section 2 no longer applies, and the compiler treats the entire union as a single, indivisible type, checked once.
// Without wrapping: distributes, checks each union member separately
type IsStringOrNumberNaked<T> = T extends string | number ? true : false;
type A = IsStringOrNumberNaked<string | number>;
// true | false — distributed per member, not the intended result
// With wrapping: treats the union as a single, non-distributed type
type IsStringOrNumberWrapped<T> = [T] extends [string | number] ? true : false;
type B = IsStringOrNumberWrapped<string | number>;
// true — the union as a whole is checked once
5. Side effect: distribution over the union never
One of the least intuitive side effects of distributive conditional types concerns the type never. Since never can be understood as an empty union with zero members, distributing over never causes the entire conditional type to immediately evaluate to never, regardless of what is in the X and Y branches. The compiler distributes the condition over zero members and consequently ends up with an empty result union.
This behavior is especially surprising for generic functions called with a potentially never-valued type parameter, for example after a previous Exclude operation that accidentally removed all members. A conditional type that expects a certain fallback behavior for never instead always receives never as the result, which can lead to hard to trace downstream errors.
type CheckNaked<T> = T extends string ? "yes" : "no";
type Result1 = CheckNaked<never>;
// never — NOT "no", because distribution over an empty union yields never
type CheckWrapped<T> = [T] extends [string] ? "yes" : "no";
type Result2 = CheckWrapped<never>;
// "no" — wrapping suppresses distribution, never is treated as a real type
6. Practical example: custom utility types with deliberate control
When writing your own utility types, you should deliberately decide for every conditional type whether distribution is desired, rather than accepting it as a random byproduct of syntax. A NonNullableDistributed<T> that removes null and undefined from every member of a union benefits from distribution and should use a naked type parameter. An IsUnion<T> that checks whether T consists of multiple members at all, on the other hand, must suppress distribution, otherwise the check itself would be undermined by distribution.
A proven pattern for IsUnion<T> uses exactly this contrast: a naked type parameter is checked against the same but artificially constrained type parameter. For a real union, distribution causes the check to fail, because a single member never covers the whole union, while a single type always passes the check.
// Deliberately distributive: strips null/undefined from every union member
type NonNullableDistributed<T> = T extends null | undefined ? never : T;
// Deliberately non-distributive: checks whether T is a union at all
type IsUnion<T, Copy = T> = T extends Copy
? [Copy] extends [T] ? false : true // if a single member covers the whole union, it's not a union
: never;
type A = IsUnion<string | number>; // true
type B = IsUnion<string>; // false
7. Interaction with generic constraints and overloads
Distribution also interacts with generic constraints in subtle ways. A generic parameter T extends SomeUnion in a function signature causes every internal conditional type with a naked T to distribute as soon as the function is instantiated with a concrete union, even when the constraint itself only describes a superset of allowed values. Developers calling a function with a single concrete type see no distribution behavior at all, because for a single type, distribution and non-distribution lead to the same result.
For overloaded functions that should return different result types for different union members, distribution is often exactly the desired behavior, because TypeScript computes the matching return type for every member individually and finally merges them into a precise union, instead of forcing a coarse union of all possible return types.
8. Debugging: when distribution strikes unexpectedly
The most common debugging case for distributive conditional types is a result type that contains a union instead of the expected single type, because a conditional type unintentionally distributed over a union. The first diagnostic step is always to check the type parameter in the conditional type: if it stands naked in the condition, distribution is the likely cause of an unexpected result.
The second common case is exactly the opposite: a result that does not contain a union even though distribution was actually expected, often because the type parameter was accidentally embedded in an object, array, or function, which means the naked type parameter rule no longer holds. A deliberate look at the exact syntactic position of the type parameter in the condition reliably resolves the vast majority of these debugging cases.
9. Distributive vs. non-distributive compared
The following table summarizes when distributive and when non-distributive behavior is the right choice, along with the corresponding syntactic patterns.
| Use case | Behavior | Pattern | Example |
|---|---|---|---|
| Filtering out union members | Distributive | T extends U ? never : T |
Exclude, Extract |
| Transformation per member | Distributive | T extends unknown ? Wrap<T> : never |
ToArrayNaked from section 2 |
| Checking the union as a whole | Non-distributive | [T] extends [U] ? X : Y |
IsStringOrNumberWrapped |
| Safely handling never | Non-distributive | [T] extends [string] ? "yes" : "no" |
CheckWrapped from section 5 |
The table shows: the choice between distributive and non-distributive behavior is not a matter of taste, it depends directly on whether the task should treat a union member by member or as an indivisible whole. Both behaviors are reachable with the same conditional type syntax, the only difference lies in whether the checked type parameter is wrapped in a tuple.
Mironsoft
TypeScript architecture, type system consulting, and refactoring
Unexplainable union types in your utility types?
We analyze existing conditional types for unwanted distribution, fix never pitfalls, and write utility types with deliberate, documented control over distribution behavior.
Type system debugging
Analysis of unexpected union results and never pitfalls in existing types
Utility type library
Tailor made Exclude, Extract, and IsUnion variants for your codebase
Code review standards
Establishing guidelines for naked vs. wrapped type parameters in your team
10. Summary
Distributive conditional types arise whenever a naked, unwrapped type parameter stands in the condition of a conditional type and is instantiated with a union type. The compiler then applies the condition to every member individually and reassembles the results into a new union, a behavior that forms the foundation of Exclude and Extract.
The tuple wrapping pattern [T] extends [U] ? X : Y deliberately suppresses distribution whenever a union should be checked as an indivisible whole, for example with IsUnion types or when safely handling never, which as an empty union would otherwise lead to surprising results. Deciding deliberately between naked and wrapped type parameters for every custom conditional type avoids the most common debugging pitfalls around distributive conditional types.
Distributive Conditional Types — the essentials at a glance
Core rule
A naked type parameter in the condition automatically distributes over union types, member by member.
Exclude & Extract
Both built in utility types rely entirely on distribution, without it they would not work.
Preventing distribution
Tuple wrapping [T] extends [U] treats the union as a single, indivisible type.
never pitfall
Distribution over the empty union never always results in never, regardless of the branch content.