Lesson 19 of 25
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.
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
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, andUncapitalizetransform 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 — likeonSavefromsave— mechanically from an existing type.
Check yourself
4 questions · pass 3/4 to unlock Modules and Declaration Files
1.What does the type `
hello-${string}` match?2.Given
type Direction = "left" | "right";and `type Move =go-${Direction}`, what does Move expand to?3.What does
Capitalize<"hello">produce?4.Combining
{ [K in keyof T ason${Capitalize<string & K>}]: () => void }withinterface T { save: void }, what key does the result have?
4 left to answer