TypeScript 44 🔷 Distributive Conditional Types
A conditional type in TypeScript has the shape T extends U ? X : Y — if T is assignable to U, the type resolves to X, otherwise to Y. That much is straightforward. The subtlety is what happens when T is a union. TypeScript does not evaluate the conditional once against the whole union. It distributes the conditional over each member of the union, evaluates each branch separately, and then unions the results. This behavior is called distributivity, and it is one of the most powerful and most surprising features of the type system. It is why Exclude<T, U> works the way it does, why NonNullable<T> strips null and undefined, and why a conditional type applied to string | number can produce a different result than the same conditional applied to each type separately and then unioned by hand. This chapter covers what distributivity is, when it fires, how to turn it off, and the practical patterns it enables.
Key point: A conditional type distributes over a naked type parameter when that parameter is a union. “Naked” means the parameter appears on the left of extends without being wrapped in a tuple, an array, or any other construct. Wrapping it — [T] extends [U] — disables distributivity and evaluates the conditional against the union as a whole. This distinction is the single most important fact about distributive conditional types, and it is the source of nearly every surprise.
What distributivity means
A distributive conditional type applies the conditional to each member of a union separately and unions the results. The rule is: if T is a naked type parameter and T is instantiated with a union, the conditional distributes.
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// Distributes: string[] | number[]
// Not: (string | number)[]
ToArray<string | number> produces string[] | number[], not (string | number)[]. The conditional was applied to string and number independently, and the results were unioned. This is distributivity in action.
Why the distinction matters. string[] | number[] is an array of strings or an array of numbers — a single array cannot mix both. (string | number)[] is an array that can hold either. These are different types with different capabilities. Distributivity is why the first is produced rather than the second, and understanding this is the difference between a type that works and one that quietly does the wrong thing.
The trigger condition. Distributivity fires when three conditions hold: the conditional is of the form T extends U ? X : Y, T is a naked type parameter (not wrapped in any construct), and T is instantiated with a union. If any of these fails, the conditional is evaluated normally against the whole type.
type NotDistributive<T> = [T] extends [unknown] ? T[] : never;
type B = NotDistributive<string | number>;
// [string | number] extends [unknown] → true
// Result: (string | number)[]
Wrapping T in a tuple [T] makes it no longer naked, so the conditional evaluates once against the union as a whole. This is the standard technique for disabling distributivity when you do not want it.
Why distributivity exists. It makes conditional types behave compositionally. If a type is a union of
AandB, a conditional that says “if this is a string, do X” should logically apply to theApart and theBpart independently. Without distributivity, every conditional type over unions would need to be hand-written to handle each member, which would be impractical. Distributivity is the default because it is the more useful behavior for the common case.
Practical patterns enabled by distributivity
Distributivity is not an edge case — it is the mechanism behind many built-in utility types. Exclude, Extract, NonNullable, and ReturnType all rely on it. Understanding distributivity means understanding how they work.
Exclude<T, U> removes members of T that are assignable to U.
type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a">;
// Distributes:
// "a" extends "a" ? never : "a" → never
// "b" extends "a" ? never : "b" → "b"
// "c" extends "a" ? never : "c" → "c"
// Union: never | "b" | "c" → "b" | "c"
Each union member is tested independently. The ones that match "a" become never, and the union of the results — never | "b" | "c" — simplifies to "b" | "c" because never is the identity of union.
Extract<T, U> keeps members of T that are assignable to U.
type Extract<T, U> = T extends U ? T : never;
type Result = Extract<"a" | "b" | "c", "a" | "b">;
// "a" | "b"
The same distribution happens, but the matching members are kept and the others become never.
NonNullable<T> removes null and undefined.
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | null | undefined>;
// string
Each member is tested against null | undefined. Strings pass and are kept; null and undefined match and become never.
ReturnType<T> is a different kind of conditional but still benefits from the same reasoning when T is a union of function types.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = ReturnType<(() => string) | (() => number)>;
// string | number
Each function type is tested independently, and the inferred return types are unioned.
Why these utilities are so useful. Every one of them is a tiny conditional type that only works because of distributivity. Writing Exclude by hand without distributivity would require enumerating every member of the union, which is impossible in a generic context. Distributivity makes the utility types compositional over arbitrary unions.
Disabling distributivity with [T]
When distributivity is not wanted, wrapping the type parameter in a tuple disables it. This is essential when you need to test the union as a whole rather than its members.
type IsNever<T> = [T] extends [never] ? true : false;
type A = IsNever<never>; // true
type B = IsNever<string>; // false
type C = IsNever<string | number>; // false
If IsNever were written as T extends never ? true : false, it would distribute over unions. never distributes to never, which makes the result never rather than true or false. The tuple wrapper prevents this and gives the expected answer.
Why IsNever needs the wrapper. never is the empty union. When a conditional type distributes over never, the result is never — distribution over an empty union produces an empty union. That makes T extends never ? true : false useless for detecting never, because the whole conditional collapses to never instead of resolving to true. The tuple wrapper [T] turns never into [never], a non-empty tuple, and the conditional then evaluates normally.
Other cases where disabling matters. Any time you want to know something about the whole union — whether it is never, whether it is a specific exact type, whether it is assignable as a unit — the tuple wrapper is the tool. Distributivity is the default; turning it off is a deliberate choice.
Distributivity over never and empty unions
The behavior of conditional types over never is a common source of confusion. When a naked type parameter is instantiated with never, the conditional distributes over the empty union and the result is never.
type Test<T> = T extends string ? "yes" : "no";
type A = Test<never>; // never, not "no"
This is surprising if you expect never extends string to be false and the result to be "no". The distribution rule applies: never is the empty union, distribution over an empty union produces never, so the whole conditional collapses.
Why this behavior is correct. never is the bottom type — the type with no values. A conditional that asks “for each member of T, is it a string?” over an empty set of members produces an empty set of results. never is that empty set. It is the identity of distribution, just as never is the identity of union.
When this matters. Utility types like Exclude<never, string> and Extract<never, string> both resolve to never, which is consistent with the rule. If you need to distinguish never from other cases, use the [T] extends [never] technique.
Distributivity and infer
Conditional types with infer also distribute. When T is a union, the infer position captures each member separately, and the results are unioned.
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[] | number[]>;
// Distributes: string | number
Each array type is tested independently, and the inferred element types are unioned. This is how ElementType extracts the element type from an array type, and it works over unions because of distribution.
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type A = Unwrap<Promise<string> | Promise<number>>;
// string | number
Again, each member of the union is unwrapped independently, and the results are unioned. The recursion and distribution combine.
What infer captures when distributing. Each distribution instantiates infer U separately. The union of all captured types is the result. This is the mechanism behind ReturnType over unions of function types and behind many utility types that inspect the shape of a type.
Complete Example Session
// ============================================
// PART 1: BASIC DISTRIBUTION
// ============================================
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
// ============================================
// PART 2: DISABLING WITH TUPLE
// ============================================
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayNonDist<string | number>;
// (string | number)[]
// ============================================
// PART 3: EXCLUDE
// ============================================
type MyExclude<T, U> = T extends U ? never : T;
type C = MyExclude<"a" | "b" | "c", "a">;
// "b" | "c"
// ============================================
// PART 4: EXTRACT
// ============================================
type MyExtract<T, U> = T extends U ? T : never;
type D = MyExtract<"a" | "b" | "c", "a" | "b">;
// "a" | "b"
// ============================================
// PART 5: NONNULLABLE
// ============================================
type MyNonNullable<T> = T extends null | undefined ? never : T;
type E = MyNonNullable<string | null | undefined>;
// string
// ============================================
// PART 6: INFER DISTRIBUTES
// ============================================
type ElementType<T> = T extends (infer U)[] ? U : never;
type F = ElementType<string[] | number[]>;
// string | number
// ============================================
// PART 7: DISTRIBUTION OVER NEVER
// ============================================
type Test<T> = T extends string ? "yes" : "no";
type G = Test<never>;
// never
// ============================================
// PART 8: DETECTING NEVER
// ============================================
type IsNever<T> = [T] extends [never] ? true : false;
type H = IsNever<never>; // true
type I = IsNever<string>; // false
// ============================================
// PART 9: EXCLUDE WITH A UNION TARGET
// ============================================
type J = MyExclude<"a" | "b" | "c" | "d", "a" | "c">;
// "b" | "d"
// ============================================
// PART 10: DISTRIBUTION AND RECURSION
// ============================================
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type K = DeepUnwrap<Promise<Promise<string>> | number>;
// string | number
Each part isolates one behavior. Parts 1 and 2 show the on/off switch, parts 3 through 5 show the utility types, parts 6 through 8 show infer, never, and detection, and parts 9 and 10 show combinations.
Quick Reference
The Distribution Rule
| Condition | Distributive? |
|---|---|
T extends U ? X : Y with T naked | ✅ |
[T] extends [U] ? X : Y | ❌ |
T[] extends U ? X : Y | ❌ |
T extends U ? X : Y with T a concrete type | N/A |
Built-in Distributive Utilities
| Utility | Definition |
|---|---|
Exclude<T, U> | T extends U ? never : T |
Extract<T, U> | T extends U ? T : never |
NonNullable<T> | T extends null | undefined ? never : T |
ReturnType<T> | T extends (...a: any[]) => infer R ? R : never |
InstanceType<T> | T extends new (...a: any[]) => infer R ? R : never |
Disabling Distributivity
| Goal | Pattern |
|---|---|
| Test the whole union | [T] extends [U] ? X : Y |
Detect never | [T] extends [never] ? true : false |
| Keep union intact | [T] extends [unknown] ? ... : ... |
Distribution over never
| Input | Result |
|---|---|
T extends U ? X : Y with T = never | never |
[T] extends [U] ? X : Y with T = never | Evaluated normally |
Common Distributive Patterns
| Pattern | Example |
|---|---|
| Remove members | T extends U ? never : T |
| Keep members | T extends U ? T : never |
| Transform each member | T extends unknown ? F<T> : never |
| Extract from container | T extends (infer U)[] ? U : never |
| Unwrap nested | T extends Promise<infer U> ? F<U> : T |
Best Practices
✅ Do This:
// Use distributivity deliberately in utility types
type Exclude<T, U> = T extends U ? never : T; // ✅
// Disable it with tuples when you need whole-union semantics
type IsNever<T> = [T] extends [never] ? true : false; // ✅
// Distribute over containers with infer
type ElementType<T> = T extends (infer U)[] ? U : never; // ✅
// Combine distribution with recursion
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T; // ✅
// Comment when distribution is intentional
type Without<T, U> = T extends U ? never : T; // ✅
❌ Don’t Do This:
// Don't expect a conditional over never to take the false branch
type Bad<T> = T extends string ? "yes" : "no";
type R = Bad<never>; // never, not "no" // ⚠️
// Don't forget the tuple wrapper when testing whole unions
type WrongIsNever<T> = T extends never ? true : false; // ⚠️
// Don't assume (string | number)[] when you wrote T[]
type Surprise<T> = T extends unknown ? T[] : never; // ⚠️
// Don't distribute over large unions carelessly
// Each member instantiates the conditional separately // ⚠️
// Don't mix distribution with mapped-type removal unexpectedly
// Removal of keys does not trigger distribution the same way // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
T extends never | Always never | Use [T] extends [never] |
Expecting (A | B)[] | Got A[] | B[] | Wrap in [T] |
Distribution over never | Result is never | Wrap or handle separately |
infer captures union | Union of all captures | Intended for extraction |
| Distribution on mapped types | Not the same rule | Mapped types iterate keys, not union members |
| Recursion over unions | Exponential cost | Bound depth, avoid large unions |
| Disabling inadvertently | Wrapped T changes semantics | Use tuple only when intended |
Confusing never and unknown | Different roles | never is empty, unknown is top |
Real-World Examples
1. Exclude in an event handler
type Events = "click" | "focus" | "blur";
type MouseEvents = Exclude<Events, "focus" | "blur">; // "click"
2. Extract for allowed values
type All = "a" | "b" | "c";
type Allowed = Extract<All, "a" | "b">; // "a" | "b"
3. NonNullable for optional inputs
function process<T>(value: NonNullable<T>) { /* ... */ }
4. Element type extraction
type ElementType<T> = T extends (infer U)[] ? U : never;
type E = ElementType<Array<string | number>>; // string | number
5. Return type over a union
type R = ReturnType<(() => string) | (() => number)>; // string | number
6. Detecting never in a generic
type IsNever<T> = [T] extends [never] ? true : false;
7. Removing null from all properties
type NoNulls<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
8. Distributing over event names
type Handler<T> = T extends string ? { type: T } : never;
type H = Handler<"click" | "focus">; // { type: "click" } | { type: "focus" }
9. Deep unwrap
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type U = Unwrap<Promise<Promise<number>>>; // number
10. Filtering a union of object types
type FilterByKind<T, K> = T extends { kind: K } ? T : never;
Visual: Distribution
┌──────────────────────────────────────────────┐
│ type ToArray<T> = T extends unknown │
│ ? T[] │
│ : never; │
│ │
│ ToArray<string | number> │
│ │
│ distributes over │
│ ┌───────────┐ │
│ │ │ │
│ ▼ ▼ │
│ string number │
│ │ │ │
│ ▼ ▼ │
│ string[] number[] │
│ │ │ │
│ └─────┬─────┘ │
│ ▼ │
│ string[] | number[] │
│ │
└──────────────────────────────────────────────┘
Visual: Disabling Distribution
┌──────────────────────────────────────────────┐
│ type ToArrayNonDist<T> = │
│ [T] extends [unknown] ? T[] : never; │
│ │
│ ToArrayNonDist<string | number> │
│ │
│ does NOT distribute │
│ │
│ [string | number] extends [unknown] │
│ │ │
│ ▼ │
│ true │
│ │ │
│ ▼ │
│ (string | number)[] │
│ │
└──────────────────────────────────────────────┘
Visual: Exclude Step by Step
┌──────────────────────────────────────────────┐
│ Exclude<"a" | "b" | "c", "a"> │
│ │
│ "a" extends "a" ? never : "a" → never │
│ "b" extends "a" ? never : "b" → "b" │
│ "c" extends "a" ? never : "c" → "c" │
│ │
│ Union of results: │
│ never | "b" | "c" │
│ │
│ never is the identity of union: │
│ "b" | "c" │
│ │
└──────────────────────────────────────────────┘
Visual: never and Distribution
┌──────────────────────────────────────────────┐
│ type Test<T> = T extends string ? "yes" : "no";│
│ │
│ Test<never> │
│ │
│ never is the empty union │
│ Distribution over empty union → empty union │
│ │
│ Result: never (not "no") │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ type IsNever<T> = [T] extends [never] │
│ ? true │
│ : false; │
│ │
│ IsNever<never> │
│ │
│ [never] extends [never] → true │
│ │
│ ✅ Detects never correctly │
│ │
└──────────────────────────────────────────────┘
Visual: Utility Types as Distributive Conditionals
┌──────────────────────────────────────────────┐
│ Exclude<T, U> T extends U ? never : T │
│ Extract<T, U> T extends U ? T : never │
│ NonNullable<T> T extends null | undefined │
│ ? never │
│ : T │
│ │
│ All three rely on distribution over T. │
│ Remove distribution and they break. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Behavior |
|---|---|
| Distributive trigger | Naked type parameter + union |
| Default | Distributive |
| Disable | Wrap in [T] |
Over never | Result is never |
With infer | Captures each member separately |
| Built-ins using it | Exclude, Extract, NonNullable, ReturnType |
Detection of never | [T] extends [never] |
| Common surprise | T[] on union gives A[] | B[] |
Key takeaways:
- Distributivity is the default for a naked type parameter in
T extends U ? X : Y - It distributes over each member of a union and unions the results
Exclude,Extract, andNonNullableare built on distributivity — understanding it means understanding them- Wrap
Tin a tuple to disable distribution —[T] extends [U]evaluates against the union as a whole - Distribution over
neverproducesnever, becauseneveris the empty union - Detecting
neverrequires the tuple wrapper —[T] extends [never] ? true : false inferdistributes too, capturing each union member separately- Disabling distributivity is a deliberate choice — know when you want whole-union semantics and when you want member-by-member
Remember: Distributivity is why conditional types compose over unions. When you write T extends U ? X : Y and T is a union, each member is evaluated independently. When you want the union treated as a single type, wrap it in a tuple. That one rule explains almost every surprising result you will encounter with conditional types.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!