C.W.K.
Stream
Lesson 05 of 05 · published

Distributive Unions: When `T | U` Becomes Weird

~10 min · unions-intersections, distribution, conditional-types, generics

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Conditional types distribute over unions. Once you know this, several mysterious type errors stop being mysteries."

The rule

When a conditional type's checked type is a naked generic type parameter, and that parameter is a union, TypeScript distributes the conditional over each member of the union and unions the results. Symbolically: T extends X ? A : B applied to T = U1 | U2 becomes (U1 extends X ? A : B) | (U2 extends X ? A : B).

This is usually what you want. Sometimes it's not. The escape hatch: wrap the parameters in brackets — [T] extends [X] — which converts the comparison to a single-element tuple test and disables distribution.

When distribution is helpful

The classic use: type-level mapping over unions. type Wrap<T> = T extends unknown ? { value: T } : never. Applied to string | number, this distributes: Wrap<string> | Wrap<number> = { value: string } | { value: number }. Each member of the input becomes its own wrapped variant in the output.

Same pattern: Exclude<T, U> from the standard library is implemented as T extends U ? never : T — distribution is what makes it remove specific members from a union.

When distribution surprises

You expect a single result and get a union. Or you expect distribution and don't get it because the type parameter isn't naked (it's wrapped in another shape). The fix in both directions:

  • To force distribution: keep the parameter naked at the conditional check site.
  • To prevent distribution: wrap it in a tuple — [T] extends [X].

The bracket trick comes up in many advanced TypeScript patterns. Once you know it exists, you'll spot it in library type definitions all the time.

The pragmatic rule: if a generic conditional type gives you weirdly-distributed results, wrap the check in brackets and see if that's what you wanted. If yes, ship it. If no, the distribution was right — read the result type carefully and figure out which input variant produced which output.

Pippa's confession

I've reached for the bracket trick maybe four times in cwkPippa — each time when I wanted a single yes/no answer about a generic type, not a distributed answer over its union members. It's not a daily-use feature, but when you need it, nothing else does the job. Worth knowing exists, even if you only use it rarely.

Code

Distribution as a feature·typescript
// Distribution in action — Wrap example.
type Wrap<T> = T extends unknown ? { value: T } : never;

type A = Wrap<string>;                    // { value: string }
type B = Wrap<string | number>;
// Distributes:
//   Wrap<string> | Wrap<number>
//   { value: string } | { value: number }

// Exclude<T, U> — built into the standard library.
type WithoutNull<T> = Exclude<T, null>;
type C = WithoutNull<string | null>;       // string

// Without distribution, Exclude couldn't work — it relies on the per-member test.
Bracket trick — disable distribution·typescript
// When distribution is NOT what you want.

// Without bracket — distributes.
type IsExactlyArray<T> = T extends any[] ? true : false;
type X1 = IsExactlyArray<string | string[]>;
//   IsExactlyArray<string> | IsExactlyArray<string[]>
//   false | true
// You wanted a single yes/no, not a union of two.

// With bracket — disables distribution.
type IsExactlyArray2<T> = [T] extends [any[]] ? true : false;
type X2 = IsExactlyArray2<string | string[]>;
//   [string | string[]] extends [any[]] ? true : false
//   false   (because string | string[] is not assignable to any[])

// The bracket converts the test to 'is this whole type assignable.'

External links

Exercise

Write a NonNullable<T> utility yourself: it should turn string | null | undefined into string. Use distribution. Then write a WrapAll<T> that turns string | number into { value: string } | { value: number }. Confirm the result types by hovering them in your editor.
Hint
type NonNullable<T> = T extends null | undefined ? never : T. Distribution makes each member of the input get tested separately, and never members are eliminated from the resulting union. WrapAll is even simpler: type WrapAll<T> = T extends unknown ? { value: T } : never.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.