AniUI Academy

Template Literal Types

Build new string types out of unions the same way template literals build strings out of values, and combine them with mapped types to generate related keys.

9 min read

Template literal types apply the same ${} interpolation syntax you already know from JavaScript template literals — but at the type level, building new string types out of existing ones instead of building strings out of values.

The basic syntax

A template literal type looks exactly like a JavaScript template literal, just written in a type position:

type Greeting = `Hello, ${string}!`;
 
let a: Greeting = "Hello, Amara!"; // fine — matches the pattern
let b: Greeting = "Hi, Amara!";
// Type '"Hi, Amara!"' is not assignable to type '`Hello, ${string}!`'.

${string} inside the type acts as a placeholder matching any string content in that position — the surrounding literal text ("Hello, " and "!") still has to match exactly.

Combining with unions: distribution

The genuinely useful behaviour appears when a union is interpolated — TypeScript expands it into every combination, one string literal per original union member:

type Direction = "left" | "right" | "up" | "down";
type Move = `go-${Direction}`;
// "go-left" | "go-right" | "go-up" | "go-down"
 
function move(action: Move) {
  /* ... */
}
 
move("go-left");  // fine
move("go-sideways");
// Argument of type '"go-sideways"' is not assignable to type 'Move'.

With two unions interpolated at once, every combination of both is generated:

type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";
 
type Variant = `${Size}-${Color}`;
// "sm-red" | "sm-blue" | "md-red" | "md-blue" | "lg-red" | "lg-blue"

Six combinations from two small unions — this scales multiplicatively, which is worth keeping in mind for very large unions, since the compiler has to enumerate every resulting literal.

Built-in string manipulation types

Four intrinsic types transform a string literal's casing, entirely at compile time — useful for exactly the kind of key-renaming seen in the mapped types lesson:

type A = Uppercase<"hello">;    // "HELLO"
type B = Lowercase<"HELLO">;    // "hello"
type C = Capitalize<"hello">;   // "Hello"
type D = Uncapitalize<"Hello">; // "hello"

These aren't runtime functions — nothing here calls .toUpperCase() on an actual string. They operate purely on string literal types, resolved entirely by the compiler.

Combining with mapped types

The real payoff, previewed at the end of the mapped types lesson: combining a template literal type with key remapping (as) generates a full set of related keys from an existing type, mechanically:

interface Actions {
  save: void;
  cancel: void;
}
 
type Handlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: () => void;
};
 
type ActionHandlers = Handlers<Actions>;
// { onSave: () => void; onCancel: () => void }
 
const handlers: ActionHandlers = {
  onSave: () => console.log("saved"),
  onCancel: () => console.log("cancelled"),
};

string & K in the middle is a small, common workaround: K on its own, inside a mapped type, is typed as string | number | symbol (since object keys can be any of those), but Capitalize only accepts string. Intersecting with string narrows K down to just its string-compatible part, which is what Capitalize needs to accept it.

This pattern — deriving onSave and onCancel from save and cancel automatically — is exactly how libraries that need a large, systematically related set of keys (event handler props in a UI library, for instance) avoid hand-writing every single one and keeping them in sync by hand.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A template literal type mirrors JavaScript template literal syntax at the type level, matching or generating string literal patterns.
  • Interpolating a union inside a template literal type distributes across every combination of its members.
  • Uppercase, Lowercase, Capitalize, and Uncapitalize transform string literal types at compile time, with no runtime equivalent involved.
  • Combined with mapped-type key remapping (as), template literal types can generate a full family of related keys — like onSave from save — mechanically from an existing type.

Check yourself

4 questions · pass 3/4 to unlock Modules and Declaration Files

up to 50
  1. 1.What does the type ` hello-${string} ` match?

  2. 2.Given type Direction = "left" | "right"; and ` type Move = go-${Direction} `, what does Move expand to?

  3. 3.What does Capitalize<"hello"> produce?

  4. 4.Combining { [K in keyof T as on${Capitalize<string & K>}]: () => void } with interface T { save: void }, what key does the result have?

4 left to answer