| |

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 A and B, a conditional that says “if this is a string, do X” should logically apply to the A part and the B part 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

ConditionDistributive?
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 typeN/A

Built-in Distributive Utilities

UtilityDefinition
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

GoalPattern
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

InputResult
T extends U ? X : Y with T = nevernever
[T] extends [U] ? X : Y with T = neverEvaluated normally

Common Distributive Patterns

PatternExample
Remove membersT extends U ? never : T
Keep membersT extends U ? T : never
Transform each memberT extends unknown ? F<T> : never
Extract from containerT extends (infer U)[] ? U : never
Unwrap nestedT 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

PitfallProblemSolution
T extends neverAlways neverUse [T] extends [never]
Expecting (A | B)[]Got A[] | B[]Wrap in [T]
Distribution over neverResult is neverWrap or handle separately
infer captures unionUnion of all capturesIntended for extraction
Distribution on mapped typesNot the same ruleMapped types iterate keys, not union members
Recursion over unionsExponential costBound depth, avoid large unions
Disabling inadvertentlyWrapped T changes semanticsUse tuple only when intended
Confusing never and unknownDifferent rolesnever 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

ItemBehavior
Distributive triggerNaked type parameter + union
DefaultDistributive
DisableWrap in [T]
Over neverResult is never
With inferCaptures each member separately
Built-ins using itExclude, Extract, NonNullable, ReturnType
Detection of never[T] extends [never]
Common surpriseT[] 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, and NonNullable are built on distributivity — understanding it means understanding them
  • Wrap T in a tuple to disable distribution — [T] extends [U] evaluates against the union as a whole
  • Distribution over never produces never, because never is the empty union
  • Detecting never requires the tuple wrapper — [T] extends [never] ? true : false
  • infer distributes 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!