Now, we get to the part where template literals stop being a cute party trick and start doing real, honest work. You’ve seen how you can bake a string pattern directly into a type. That’s useful, but its power is almost limitless when you combine it with union types. This is where you move from describing a single, specific string to describing an entire family of possible strings. It’s the difference between having a single key and having the master keyring for the entire building.

The core concept is beautifully simple: when a union type is used inside the template literal’s ${...}, TypeScript doesn’t just see the union type itself. It performs a distributive operation. It says, “Okay, for every member of this union, I’m going to generate a new string literal type by plugging it into this template.” The result is a new union type of all possible combinations.

The Distributive Magic

Let’s start with a classic, almost canonical example. Imagine you’re building a design system or CSS-in-JS library. You need types for all possible padding or margin directions.

type VerticalDirection = 'top' | 'bottom';
type HorizontalDirection = 'left' | 'right';

type Direction = VerticalDirection | HorizontalDirection;

// The magic happens here:
type PaddingProperty = `padding-${Direction}`;

// This evaluates to:
// type PaddingProperty =
//   | "padding-top"
//   | "padding-bottom"
//   | "padding-left"
//   | "padding-right";

See what happened? TypeScript didn’t create a type like `padding-${'top' | 'bottom' | 'left' | 'right'}`. That would be meaningless. Instead, it distributed the union across the template. It generated a new string for each constituent of the Direction union. This is the engine that drives most of your template literal type workflows.

Generating All Combinations with Two Unions

But wait, it gets better. What if you have two unions? The distribution works there, too, creating a combinatorial explosion of every possible pairing. This is how you build out exhaustive sets of class names or event names.

type Size = 'sm' | 'md' | 'lg';
type Component = 'btn' | 'alert' | 'card';

type UtilityClass = `${Component}--${Size}`;

// This evaluates to:
// type UtilityClass =
//   | "btn--sm"
//   | "btn--md"
//   | "btn--lg"
//   | "alert--sm"
//   | "alert--md"
//   | "alert--lg"
//   | "card--sm"
//   | "card--md"
//   | "card--lg";

This is incredibly powerful for ensuring your design system’s classes are used correctly. You can now have a function, getClass(component: Component, size: Size), that returns a value of type UtilityClass, and it’s completely type-safe. No more "buttom--medium" typos.

The Empty Union Trap

Here’s your first “gotcha.” It’s a logical one, but it can bite you. What happens if the union you’re distributing over is… nothing?

type Nowhere = never;
type Example = `prefix-${Nowhere}`; // Type is... never.

If your union type is never (i.e., it has no members), the template literal type that uses it also evaluates to never. There are no members to distribute, so you get nothing. This is often useful, but can be surprising if a generic type parameter defaults to or becomes never.

Working with Primitive Types

You’re not limited to unions of string literals. You can use other primitives, but the result is always a string literal type. TypeScript will happily convert them for you.

type NumberUnion = 1 | 2 | 3;
type NumberInString = `id-${NumberUnion}`; // "id-1" | "id-2" | "id-3"

type BooleanUnion = true | false;
type BooleanInString = `flag-${BooleanUnion}`; // "flag-true" | "flag-false"

This is straightforward, but be mindful of the output. You’ll get the string representation of the value, which is what you’d expect. The real power, and the real headaches, come when you start trying to do the inverse—parsing these strings back into their constituent parts. But that’s a topic for the next section, where we’ll get into conditional types and inference. For now, just revel in your newfound ability to generate massive, perfectly typed sets of strings with almost no effort. It’s like a code generator, but it’s just the type system.