Template literal types — string manipulation at the type level
TypeScript's template literal types use the exact same backtick syntax as a JavaScript template string, but at the TYPE level — letting a type be built out of a pattern of other types, which is what makes it possible to statically validate that a string actually matches a specific shape.
3 min read
The same syntax you already know, moved into type position
type Greeting = `Hello, ${string}!`;
const a: Greeting = "Hello, Ada!"; // valid — matches the pattern
const b: Greeting = "Hi, Ada!"; // TYPE ERROR — doesn't match "Hello, ${string}!"`Hello, ${string}!` as a type means exactly what it looks like: any string that starts with "Hello, ", ends with "!", and has anything (string) in between. This is genuinely new capability the earlier lessons' type system doesn't have on its own — string alone accepts any string at all; a template literal type narrows that down to strings matching a specific, checkable pattern, entirely at compile time.
Combining template literal types with unions: every valid combination, generated automatically
type Size = "small" | "medium" | "large";
type Color = "red" | "blue";
type Variant = `${Size}-${Color}`;
// Variant is automatically: "small-red" | "small-blue" | "medium-red" |
// "medium-blue" | "large-red" | "large-blue"
const v: Variant = "medium-blue"; // valid
const invalid: Variant = "huge-blue"; // TYPE ERROR — "huge" isn't a valid SizeWhen a template literal type's placeholders are themselves union types, TypeScript automatically generates every combination as the resulting type — six combinations here, computed once from two much shorter unions, rather than needing to be written out by hand. This scales the same way for larger unions: the type system does the combinatorial work, and the result stays exactly as strict as if every combination had been listed individually.
A real, practical use: typing event names and CSS-in-JS keys precisely
type EventName = `on${Capitalize<"click" | "hover" | "focus">}`;
// EventName: "onClick" | "onHover" | "onFocus"
function addHandler(event: EventName, handler: () => void) { /* ... */ }
addHandler("onClick", () => {}); // valid
addHandler("onclick", () => {}); // TYPE ERROR — wrong casing, caught at compile timeCapitalize<T> (one of TypeScript's built-in string-manipulation utility types, alongside Uppercase, Lowercase, and Uncapitalize) transforms each string literal in a union, and combining it with a template literal type produces a precise, exhaustive set of valid event-handler names — a typo in casing ("onclick" instead of "onClick") is now a real compile-time error, not a bug that only shows up when the handler silently never fires at runtime.
Why this genuinely differs from just using string
// Loose — accepts ANY string, including typos and nonsense
function setRoute(path: string) { /* ... */ }
setRoute("/usres"); // typo — compiles fine, fails only at RUNTIME
// Precise — only accepts strings matching the actual defined pattern
type Route = `/${"users" | "posts" | "settings"}`;
function setRoute(path: Route) { /* ... */ }
setRoute("/usres"); // TYPE ERROR — caught before the code ever runsUsing plain string for something that actually only has a small, known set of valid shapes (route paths, event names, CSS class name patterns) means TypeScript can't catch a typo or an invalid value at all — it's syntactically a valid string, so it type-checks, and the mistake only surfaces at runtime. A template literal type (often combined with unions, as shown throughout this lesson) narrows the type down to the genuinely valid set, catching exactly the kind of typo that's easy to make and easy to miss in review, at compile time instead.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does the type `` `Hello, ${string}!` `` actually represent?
2. What happens when a template literal type's placeholder is filled with a union type, like `` `${Size}-${Color}` ``?
3. Why does using a template literal type for route paths catch more bugs than using plain `string`?