Distributive Conditional Types in TypeScript
When a conditional type receives a union, TypeScript distributes the check over each member. Learn how distribution works, when to use it, and how to disable it.
TypeScript distributive conditional types are what happens when a conditional type receives a union. Instead of checking the union as a single type, TypeScript applies the condition to each member of the union separately, then combines the results back into a union.
Here is the simplest demonstration:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// ^? string[] | number[]TypeScript evaluates the condition for each union member separately and combines the results into string[] | number[]. This distributive behavior is applied automatically whenever the checked type in a conditional type is a union, without any extra syntax.
This Mermaid diagram shows what happens step by step:
TypeScript splits the union, evaluates the conditional for each member, and unions the outcomes. This is the default and usually desired behavior for generic conditional types.
When distribution happens
Distribution happens only when the checked type is a bare generic type parameter. A bare type parameter is just T, not T wrapped in something:
type Dist<T> = T extends any ? T[] : never; // distributive
type NoDist<T> = [T] extends [any] ? T[] : never; // not distributiveIn Dist the type parameter is bare, so it distributes. In NoDist it is wrapped in a tuple, so TypeScript treats the whole union as one value instead of splitting it apart. Passing the same union to both shows the difference clearly:
type A = Dist<string | number>;
// ^? string[] | number[]
type B = NoDist<string | number>;
// ^? (string | number)[]NoDist checks the entire union against any in one shot and produces a single array type whose element type is itself a union, rather than a union of two separate array types.
Other wrappers that disable distribution include T & {}, T | never, and { prop: T }. Any transformation that makes the parameter not bare disables distribution.
Built-in types that use distribution
Several built-in utility types rely on distributive behavior:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;Exclude removes types from a union. When the first type parameter is string | number | boolean and the second is string or boolean, TypeScript distributes over the first union and checks each member against the second:
type Result = MyExclude<string | number | boolean, string | boolean>;
// ^? numberThe step-by-step:
| Member | Check | Result |
|---|---|---|
| string | extends string or boolean → true | never |
| number | extends string or boolean → false | number |
| boolean | extends string or boolean → true | never |
Members that match vanish from the union, leaving only number. Extract works the same way but keeps the matching members instead of discarding them, which makes it the mirror image of Exclude:
type Result = MyExtract<string | number | boolean, string | boolean>;
// ^? string | booleanNonNullable applies the identical distributive pattern to strip null and undefined values out of a union, which is useful right after narrowing an optional value:
type Clean = MyNonNullable<string | null | undefined>;
// ^? stringAll three utility types exist because distribution lets a conditional type process every union member on its own, then reassemble the results.
The never behavior
The type never is the empty union. When a distributive conditional type receives never, there are no members to distribute over, so the result is never as well:
type Check<T> = T extends string ? "yes" : "no";
type Result = Check<never>;
// ^? neverThis can be surprising if you expect the false branch to run. Since there are no members, the conditional does not run at all, and TypeScript falls back to never.
To avoid this, disable distribution by wrapping the type parameter in square brackets:
type CheckSafe<T> = [T] extends [string] ? "yes" : "no";
type Result = CheckSafe<never>;
// ^? "yes"With [T], never is checked as a single type instead of a set of members to distribute over. Here the check passes because never is assignable to every type, so the true branch runs.
When to disable distribution
Distribution is usually what you want, but not always. Disable it when you need to check the union as a single type rather than its individual members.
A common case is when you want to check whether an entire type "contains" or "matches" a pattern, rather than filtering its members:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[] | number>;
// ^? booleanThe result is true | false (which collapses to boolean) because distribution runs the check on string[] and number separately, once per member. If you wanted to ask "is this union itself an array?", you would disable distribution:
type IsArrayUnion<T> = [T] extends [any[]] ? true : false;
type B = IsArrayUnion<string[] | number>;
// ^? falseNow the whole union is checked as one type, and a union of an array and a non-array is not itself an array.
Distributive conditional types with infer
When infer is used inside a distributive conditional type that receives a union, distribution applies first, and then infer captures a piece from each member on its own:
type ElementType<T> = T extends Array<infer Item> ? Item : never;
type Result = ElementType<string[] | number[]>;
// ^? string | numberTypeScript distributes over string[] and number[] separately, capturing string from one and number from the other, then unions the captured types. For more on the capture mechanism itself, see Infer Keyword in TypeScript.
Filtering object types with distribution
You can combine distribution with conditional checks on object shapes to filter a union of object types:
type HasId = { id: number };
type WithId<T> = T extends HasId ? T : never;
type A = { id: number; name: string };
type B = { title: string };
type C = { id: number; age: number };
type Filtered = WithId<A | B | C>;
// ^? A | CThe B type does not have an id property, so the check fails and it maps to never, vanishing from the result.
Common mistakes
Expecting distribution with non-bare type parameters. { p: T } extends { p: U } does not distribute. Only a bare type parameter triggers distribution.
Assuming never is handled like other types. A distributive conditional type returns never when it receives never, because there are no members to process. Wrap in [T] if you need to detect it directly.
Using distribution for runtime behavior. Distributive conditional types are purely a compile-time mechanism. They do not affect runtime values or control flow.
Writing overly complex distributive types. When a single distributive conditional type grows beyond a few lines, consider breaking it into smaller named type aliases. This makes the logic easier to test and understand.
Rune AI
Key Insights
- Distribution splits a union and applies the conditional to each member separately.
- Bare generic type parameters trigger distribution; wrapped ones do not.
Exclude,Extract, andNonNullableall rely on distribution.- Wrap in
[T]to disable distribution and check a union as a whole. neveris the empty union; distributing over it always producesnever.
Frequently Asked Questions
What makes a conditional type distributive?
How do I disable distributive behavior?
Conclusion
Distributive conditional types apply a check to every member of a union individually and union the results. This is the default for bare generic type parameters and it powers the built-in Exclude, Extract, and NonNullable types. When you need the opposite behavior, wrap the types in square brackets.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.