Now, let’s talk about something that feels like a magic trick the first time you see it: how TypeScript distributes union types over generics. This isn’t just academic; it’s the secret sauce that makes your conditional types and mapped types work predictably with unions. It’s also the reason you sometimes get results that make you scratch your head and mutter, “Well, that’s not what I meant.”

Here’s the core idea. When you have a generic type T that is a union (e.g., T extends A | B), and you use it in a naked type parameter context (more on that in a second), TypeScript doesn’t just evaluate the whole union at once. Instead, it says, “Okay, let’s take this union apart, apply the operation to each constituent individually, and then smash the results back together into a new union.” This is called distributive conditional types.

What’s a “Naked” Type Parameter?

This is the crucial bit. Distribution only happens when the type parameter appears on its own, not wrapped inside another type. It has to be “naked.” Think of it as the difference between the type parameter going to work in its pajamas versus putting on a suit.

// Naked - T is all by itself. Distribution WILL happen.
type Naked<T> = T extends string ? 'a' : 'b';

// Not Naked - T is wrapped in an array. Distribution will NOT happen.
type Wrapped<T> = [T] extends [string] ? 'a' : 'b';

// Let's test it:
type TestUnion = string | number;

type NakedResult = Naked<TestUnion>; // "a" | "b"
type WrappedResult = Wrapped<TestUnion>; // "b"

See what happened? For Naked<T>, TypeScript distributed the union:

  1. string extends string ? 'a' : 'b''a'
  2. number extends string ? 'a' : 'b''b'
  3. Union the results: 'a' | 'b'

For Wrapped<T>, it evaluated the entire union [string | number] against [string]. Since [string | number] is not a more specific version of [string], it evaluated to 'b'. This is a common source of “why isn’t my conditional type working?!” moments. You write a condition expecting distribution, but you accidentally wrapped T in a tuple, an array, or a promise, shutting the whole mechanism down.

The never Type and Empty Unions

This is where it gets properly absurd, but in a logically consistent way. What happens if you distribute over a type that includes never? Or what if you have an empty union? (Yes, that’s a thing).

Since distribution applies the operation to each member, and never is the empty union, distributing over never is… doing nothing. It’s like asking a robot to paint zero cars. The operation never runs, so it resolves to never itself.

type DistributedExample<T> = T extends string ? T : never;
type Result = DistributedExample<never>; // never

This is actually desirable behavior. It means never effectively vanishes in distributive contexts, which is exactly what you want when filtering types. You don’t want never mucking up your beautifully crafted union.

Why This is Actually Useful

This isn’t just a quirky language feature; it’s your best friend for building powerful, flexible type utilities. The most classic example is the Extract and Exclude utility types, which are built into TypeScript. You could implement them yourself like this:

// Get all types from T that are assignable to U
type MyExtract<T, U> = T extends U ? T : never;

// Get all types from T that are NOT assignable to U
type MyExclude<T, U> = T extends U ? never : T;

type Test = string | number | boolean | null;

type StringsAndNumbers = MyExtract<Test, string | number>; // string | number
type NotStrings = MyExclude<Test, string>; // number | boolean | null

Without distribution, this would be impossible. MyExclude works by taking the union T, and for each member, checking if it fits U. If it does, it maps it to never (making it disappear), and if it doesn’t, it maps it to itself. The results are unioned, giving you a clean new set.

When to Prevent Distribution

Sometimes, you want to check the entire union’s shape. This is when you deliberately break the “naked” rule. A common pattern is to wrap your type in a tuple to prevent distribution and force a whole-union check.

// A utility to check if T is exactly never, which is notoriously tricky because of distribution.
type IsNever<T> = [T] extends [never] ? true : false;

// This works because we're comparing the structure of [never] to [never]
type A = IsNever<never>; // true
type B = IsNever<string>; // false
type C = IsNever<string | never>; // false! Because it becomes IsNever<string>, which is false.

// Without the tuple, it would distribute over never and fail.
type BrokenIsNever<T> = T extends never ? true : false;
type D = BrokenIsNever<never>; // never (because it distributes over nothing!)

The moral of the story? Always be aware of whether your type parameter is naked. It dictates everything. If your conditional type is behaving mysteriously, the first thing you should do is check if you’ve accidentally dressed T for the day, preventing it from doing its distributive job. It’s a sharp tool, and like all sharp tools, you can build incredible things with it once you learn to avoid the blade.