Now, let’s get to the fun part: what happens when you throw a mapped type at a union? The short answer is magic. The slightly longer answer is that TypeScript performs a distributive operation that feels like it’s doing your laundry and folding it for you. It’s one of the language’s most elegant features, and once you understand it, you’ll start seeing patterns everywhere.

Here’s the gist: when you map over a union type, the operation distributes over each member of the union. It’s as if you applied the mapped type to each individual type in the union and then smooshed the results back together into a new union.

Let’s start with a classic example. You have a union of some primitive types.

type MyUnion = 'foo' | 'bar' | 42;

// Let's map it to an object where each property is the original value
type WrappedUnion = {
  [K in MyUnion]: { value: K };
};

What do you think WrappedUnion becomes? If you guessed a single object type, you’d be wrong, and I’d gently mock you for it. Instead, the mapping distributes:

// The result is a union of object types
type WrappedUnion =
  | { value: 'foo' } // K = 'foo'
  | { value: 'bar' } // K = 'bar'
  | { value: 42 };   // K = 42

This is incredibly powerful. It means you can take a union of keys and instantly create a union of objects that correspond to those keys. This is the fundamental mechanic behind turning a const object’s keyof into a discriminated union, which is basically the party trick that makes modern TypeScript so damn useful.

The Distribution Mechanic (It’s Not Magic, It’s Just Code)

This doesn’t happen by accident. It’s a specific behavior baked into the language for mapped types that iterate over a type parameter (a generic). This is the most important rule to engrave on your desk: Distribution only occurs when the type you’re mapping over is a naked type parameter.

Watch closely. The first example uses a type parameter T, so it distributes. The second uses the concrete type MyUnion directly, so it does not.

// Distributive mapped type (the good stuff)
type Distributive<T> = {
  [K in T]: { value: K };
};
type Result1 = Distributive<MyUnion>; // { value: 'foo' } | { value: 'bar' } | { value: 42 }

// Non-distributive mapped type (often useless)
type NonDistributive = {
  [K in MyUnion]: { value: K };
};
/* type NonDistributive = {
    foo: { value: 'foo' };
    bar: { value: 'bar' };
    42: { value: 42 };
} */

See the difference? The non-distributive version creates a single object type with properties foo, bar, and 42. That’s weird and almost never what you want. The distributive version, using the generic T, creates the clean union we’re after. This is why you’ll almost always see these patterns defined as generic types.

Escaping Distribution with Clumsy But Effective Tricks

Sometimes, distribution is a pain in the neck. You might want to map over the union as a single, collective entity. A classic example is wanting to wrap the entire union in something, like a Promise.

Your first instinct might be this, and it will backfire gloriously:

type PromisifyDistributive<T> = Promise<T>;
type Result = PromisifyDistributive<string | number>; // Promise<string> | Promise<number>

Thanks to distribution, you get a union of promises, which is not the same as a promise of a union. To stop this behavior, you have to break the “naked type parameter” rule. The standard way is to clothe the parameter in a tuple, effectively hiding its nakedness from the distributive mechanic.

type PromisifyNonDistributive<T> = Promise<[T]>[0]; // 🤦‍♂️
// Better, more readable approach:
type PromisifyNonDistributive<T> = Promise<T extends any ? T : never>;

Yes, the T extends any ? T : never trick looks like a no-op, but it’s not. It forces TypeScript to evaluate T as a whole, not as a distributable union. It’s a hack, but it’s our hack.

type Result = PromisifyNonDistributive<string | number>; // Promise<string | number>

It’s verbose and a bit silly, but it works. You’re essentially telling TypeScript, “I see your fancy distribution feature, and I respectfully decline.”

The never Gotcha: When Unions Vanish

Here’s a fun edge case that will bite you eventually. What happens if you have a union that includes never? Or if your mapping operation produces never for one of the members?

never is the empty set. When it appears in a union, it gets absorbed. It vanishes. So if your distributive mapping produces a never for one member, that member is simply removed from the resulting union. This is often used as a clever filtering mechanism.

type FilterStrings<T> = T extends string ? T : never;
type Example = FilterStrings<'hi' | 42 | 'bye' | []>; // 'hi' | 'bye'

// The mapping effectively did this:
// 'hi' extends string ? 'hi' : never --> 'hi'
// 42 extends string ? 42 : never --> never (vanishes)
// 'bye' extends string ? 'bye' : never --> 'bye'
// [] extends string ? [] : never --> never (vanishes)

This is how utility types like Extract and Exclude work under the hood. They’re just distributive conditional types that let never prune the unwanted members from the union. It’s not a bug; it’s a feature, and a brilliantly designed one at that. Just be aware that if you see never popping up in your mappings, it might be silently removing things from your results.