Topic 9.2
Distributive Conditional Types
In one line
When a conditional type is given a union, it runs once for each member and joins the answers; wrapping the type in [T] turns that off, and never (an empty union) gives an empty answer.
Think of it like this
A cashier with a basket of mixed fruit. If you say "price the basket", she doesn't price the basket as one thing. She picks up each fruit, prices it on its own, and then adds up the results. A distributive conditional type treats a union like that basket: it takes the members out one by one, runs the question on each, and puts the answers back together as a new union.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Union
- A type that allows several types, like
string | number. A value is one of the members. - Member
- One of the types joined in a union.
stringandnumberare the members ofstring | number. - Distributive
- Spread over each member. A conditional type runs once per member of a union and joins the results.
- Naked type parameter
- A type parameter standing alone before
extends, likeT. Wrapping it ([T],T[]) makes it not naked. - never
- The type with no values at all: the empty type. In a union it disappears, and as a distributed input it gives no results.
- Filter (on unions)
- Keep some members and drop others by mapping the unwanted ones to
never. - Tuple wrapper
- Writing
[T] extends [U]so the union is checked as one piece instead of being split.
Step by step
01Pass a union and watch it split
ToArray<T> turns a type into an array of it. We write the test as T extends unknown, which is always yes, just so the type is a conditional type and can distribute.
With string | number the checker runs the question for string, then for number, and joins the answers: string[] | number[]. Two separate array types, not one array of both.
type ToArray<T> = T extends unknown ? T[] : never;
type Each = ToArray<string | number>;
// string[] | number[]02Wrap it to keep the union whole
Write [T] extends [unknown] and the checked type is the tuple [string | number]. It's a single thing, so there is nothing to distribute, and T[] is (string | number)[].
Only the left side matters for the rule, but writing both sides as tuples reads clearly and keeps the question meaning "does the whole thing fit".
type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;
type Whole = ToArrayWhole<string | number>;
// (string | number)[]03Filter a union by returning never
OnlyStrings<T> keeps T when it is a string and returns never otherwise. Every never disappears when the answers are joined. The result is the union of the strings only.
That is exactly how the built-in Extract works. Exclude is the same with the two branches swapped. You'll rebuild both in the second live example.
type OnlyStrings<T> = T extends string ? T : never;
type Kept = OnlyStrings<"a" | 1 | "b" | true>;
// "a" | "b"04The empty union
never has no members, so a distributive type has nothing to run on and returns never. Passing never to ToArray gives never, while the wrapped version gives never[] (an array of nothing, which is a real type).
That's why IsNever must be written with brackets. The unwrapped version is a bug that looks like a correct type.
type IsNeverBad<T> = T extends never ? true : false;
type IsNever<T> = [T] extends [never] ? true : false;
type Bad = IsNeverBad<never>; // never (not true!)
type Good = IsNever<never>; // true05Know which case you want
Ask: "do I want an answer for each member, or one answer about the whole union?" Mapping or filtering a union wants distribution. Asking "is this entire type X?" wants the tuple wrapper.
When in doubt, test with // ^?: pass A | B and see whether the result has one piece or two.
Try it yourself
- 1
Predict the doubled boolean
Add
type X = ToArray<boolean>;with a// ^?to the first example. Predict the result before you look.booleanis the uniontrue | false, so the answer isfalse[] | true[]. - 2
Distribute keyof
Recall from Topic 8.1:
keyof Tis the union of the key names ofT. Writetype KeysOf<T> = T extends unknown ? keyof T : never;and apply it to{ a: 1 } | { b: 2 }. Compare with the plainkeyof ({ a: 1 } | { b: 2 }).Distribution gives
"a" | "b"(the keys of every member). The plainkeyofof a union gives only the keys common to all members, which isneverhere. - 3
Write your own filter
Using only a conditional type, write
NonStrings<T>that drops the strings from a union. Test it on"a" | 1 | true. Then check it against the built-inExclude<"a" | 1 | true, string>.
Code & diagrams
Each is two array types, Whole is one array of both. Passing never gives never for the distributive type, but an array of nothing for the wrapped one. Bad is never, not true, which is the trap.
Console
(nothing was printed)The built-in Exclude gives the same answer as your own. Extract keeps the members of a union of object types that have a radius or a side, so the dot shape is dropped.
Console
(nothing was printed)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Ask about never without the brackets
In the first example, IsNeverBad<never> uses T extends never ? true : false without the tuple wrapper.
Myth vs fact
Myth
A conditional type always checks the whole type I pass.
Fact
Only when the checked type is not a naked type parameter. A bare T that receives a union is split into its members first.
Myth
never in a union is an error to avoid.
Fact
It is the identity of a union: string | never is just string. Returning never from one branch is the standard way to remove members.
Myth
boolean is a single simple type, so it never distributes.
Fact
It is true | false, so a distributive conditional type runs twice and may give a union of two results.
Myth
Wrapping with [T] changes the answer to the question.
Fact
It changes only whether the union is split first. [string] extends [string] is still yes. What changes is that the question is asked once for the whole union.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Distribution happens at instantiation time: when the alias is given a concrete union, the checker maps the conditional type over its members. If
Tis still an unknown parameter (inside a generic function), nothing is split yet. That's why the same helper can look different when called with a union written in the code and with a union that arrives via a generic. - ▸
The built-in
Exclude,ExtractandNonNullableall rely on distribution (Topic 10.2).NonNullable<T>isT & {}in recent versions, which does the same job through an intersection with{}instead of a conditional type. Checking the definition inlib.es5.d.tsis the quickest way to see what a built-in does. - ▸
Large unions are expensive: a distributive type over a union of 1000 members creates 1000 instantiations. Union sizes are also capped: when a union would reach 100,000 members, tsc reports
TS2590: Expression produces a union type that is too complex to represent.(we reproduced it with five blanks of ten digits each, 10 × 10 × 10 × 10 × 10 = 100,000, in TypeScript 6.0 and 7.0). Template literal types that multiply unions (Topic 9.4) can hit this quickly.
Remember this
- 1
The problem this solves: many types are unions, and often you want to treat each member on its own: keep only the strings, turn each member into an array, or drop
null. Writing that by hand for every union would be endless. Recall from Topic 9.1: a conditional type picks one of two answers. Given a union, it can do that once per member, automatically. - 2
A union type like
string | numbermeans "a value that is one of these" (Topic 4.1). When you pass a union to a conditional type whose checked type is a bare type parameter (T extends ...with nothing around theT), TypeScript splits the union.ToArray<string | number>becomesToArray<string> | ToArray<number>, which isstring[] | number[]. This is called distribution, because the condition is spread over the members, the way2 * (a + b)becomes2a + 2b. - 3
Distribution only happens for a naked type parameter: the thing before
extendsis exactlyT, notT[], not[T], notkeyof T. It also only happens when the type alias is instantiated with a union. This is a rule about how the checker works, not about syntax you can see, so it surprises people. If you can't tell whether a type distributes, add a// ^?and pass in a union. - 4
Why it is useful. Distribution is what makes
Exclude<T, U>work.T extends U ? never : Truns on every member ofT: the members that fitUbecomeneverand vanish from the union (Topic 5.5:neverdisappears in a union), the others stay. With one rule, a conditional type becomes a filter and a mapper for unions. You can see it filter"a" | 1 | "b" | truedown to only the strings. - 5
Turning distribution off. Sometimes you want the union treated as one whole thing. Wrap both sides in square brackets:
[T] extends [U] ? X : Y. Now the checked type is a tuple[T], not a nakedT, so there is nothing to spread.ToArrayWhole<string | number>gives(string | number)[], one array that can hold either, instead of two separate array types. Pick the wrapped form when the question is about the union as a whole. - 6
**
neveris an empty union.** A union with zero members has nothing to distribute over, so the conditional type runs zero times and the result isnever. That's the famous trap:IsNever<T> = T extends never ? true : falsegivesnever, nottrue, when you passnever. The answer is to wrap:[T] extends [never] ? true : false. Now the checked type is a one-element tuple, which is a real type and gets a real answer. - 7
booleanis secretly the uniontrue | false, so it distributes too.ToArray<boolean>isfalse[] | true[]. Same for enums and any union of literals. Remember this whenever a result looks "doubled" and you passedboolean.
Explain it without notes
What is a distributive conditional type?
How do you stop a conditional type from distributing, and why would you?
Why does type IsNever<T> = T extends never ? true : false give never for IsNever<never>?
How is Exclude<T, U> built, and why does it work on a union?
Practice
Write NonStrings<T> that removes the string members from a union. Show its result for "a" | 1 | true.
Write IsUnion<T> that is true for string | number and false for string. (Hint: compare T with a distributed copy of itself.)
Write type KeysOfAll<T> that gives every key of every member of a union of objects, then test it with { a: 1 } | { b: 2 }.
Trade-offs
- ↔
Distribution gives you filter and map over unions for free, but it is invisible: there is no keyword for it. Readers must know the rule. A short comment or a
// ^?example next to the type saves them. - ↔
Wrapping in
[T]fixes theneverand "whole union" cases, but it also means the type no longer maps over members. Choose one behaviour per helper and name it so the choice is obvious (IsNever,ToArrayWhole).
Done when you can
I can say when a conditional type distributes and when it doesn't.
I can predict the result of a distributive type applied to a union.
I can turn distribution off with
[T] extends [U].I can explain why
IsNevermust use brackets.I can rebuild
ExcludeandExtractfrom memory and explain why they work.