Right, so you’ve finally met conditional types. You’re feeling clever, building types that shift and change like magic. And then you hit this. You write what looks like a perfectly reasonable type, pass it a union, and suddenly it’s like your type has been put through a blender. You get back a union of results instead of the single, unified type you expected. Welcome to the world of distributive conditional types. It’s the language’s most common “gotcha,” and it’s about to make a lot more sense.

Here’s the thing: TypeScript isn’t being difficult on purpose. This behavior is actually a feature, not a bug. It’s designed this way for a very good reason. When you write a conditional type where the type you’re checking (the part before extends) is a naked type parameter, TypeScript automatically distributes that condition over each member of a union.

Let’s look at the classic, almost-too-simple example that makes everyone’s head spin at least once.

type ToArray<T> = T extends any ? T[] : never;

// What you might naively expect:
type Result = ToArray<string | number>; // (string | number)[]

// What you *actually* get:
type ActualResult = ToArray<string | number>; // string[] | number[]

See that? We didn’t get an array that can hold both strings and numbers (string | number)[]. We got a union of array types: string[] | number[]. This is distribution in action. It’s as if TypeScript did this:

ToArray<string | number> 
// becomes...
ToArray<string> | ToArray<number>
// which becomes...
(string extends any ? string[] : never) | (number extends any ? number[] : never)
// which becomes...
string[] | number[]

This is incredibly useful! It’s the mechanism that makes types like Exclude and Extract possible. You’re essentially mapping over each element of the union.

Why in the world would I want to disable this?

Because sometimes you don’t want to map over the union. Sometimes you want to treat the entire union as a single, monolithic type. The most common use case is when you’re doing an infer extraction inside a conditional type. Distribution will often pull the rug out from under you.

Imagine you’re trying to write a type that gets the return type of a function, but also handles unions of functions.

type ReturnTypeUnion<T> = T extends (...args: any[]) => infer R ? R : never;

declare function getString(): string;
declare function getNumber(): number;

type UnionOfReturns = ReturnTypeUnion<typeof getString | typeof getNumber>;
// Evaluates to: string | number

This works perfectly. But watch what happens if we try to get the return type of a function that itself returns a union.

declare function getStringOrNumber(): string | number;

type SingleReturn = ReturnTypeUnion<typeof getStringOrNumber>;
// You want: string | number
// You get: string | number
// It works! So what's the problem?

It works here because T is a single function type, not a union. The distribution doesn’t trigger. The problem arises when your conditional type becomes more complex and you need to prevent distribution to avoid a double-union scenario.

The trick: boxing your type parameter

So how do you turn this “feature” off? You break the rule for distribution. Remember, distribution only happens if the type being checked is a naked type parameter. The solution is to clothe it. Put it in a tuple, an array, a promise, or any other surrounding type. This makes it no longer “naked.”

The most common and elegant pattern is to use a tuple:

type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Result = ToArrayNonDistributive<string | number>;
// Type is (string | number)[]

Boom. Distribution disabled. By wrapping T in [T] and checking if it extends [any], we’ve sidestepped the automatic behavior. We’re now asking “Does the tuple type [T] extend the tuple type [any]?” Since [any] is a very broad tuple type, the condition is true, and we return T[], where T is the full, untouched union string | number.

Let’s apply this to a more realistic example. Let’s say you want a type that checks if a type is exactly never. You can’t just do T extends never because that distributes over a union, and never is the empty union, so it… does nothing. It’s weird.

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // type X = never

Well, that’s useless. The distribution has nowhere to go, so the whole type evaluates to never. Let’s box it:

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type X = IsNeverFixed<never>; // type X = true
type Y = IsNeverFixed<string>; // type Y = false

Perfect. Now it works because we’re checking the entire construct, not distributing over its (non-existent) members.

The key takeaway is this: distribution is your friend 90% of the time. It’s what gives conditional types their power over unions. But for that other 10%, when you need to treat the union as a single entity, remember the boxing trick. Wrap your type parameter in a tuple [T] to make it play by your rules. It’s the difference between mapping over a list and looking at the whole list at once. And now you know how to do both.