TypeScript Template Literal Types Explained
Template literal types let TypeScript build string types from other types, like JavaScript template strings. How they work, with practical patterns.
A template literal type is a TypeScript type built with the same backtick syntax as a JavaScript template string, except the interpolated parts are types instead of values. `hello-${string}` describes every string that starts with hello-, and `${"get" | "post"}-request` expands to the union "get-request" | "post-request". They turn string types from an opaque blob into something the compiler can construct, narrow, and validate.
The basic syntax
The syntax mirrors runtime template strings, but each ${} slot holds a type instead of an expression:
type Greeting = `Hello, ${string}!`;
const a: Greeting = "Hello, world!"; // OK
const b: Greeting = "Hi there!"; // Error
Interpolating a union type distributes across every combination, producing a union of literal strings:
type Size = "small" | "medium" | "large";
type Variant = "primary" | "secondary";
type ButtonClass = `btn-${Variant}-${Size}`;
// "btn-primary-small" | "btn-primary-medium" | "btn-primary-large"
// | "btn-secondary-small" | "btn-secondary-medium" | "btn-secondary-large"
Six combinations from two small unions — this is the pattern that makes template literal types worth reaching for whenever a set of string identifiers is really the cartesian product of a few smaller sets.
Deriving event handler names
A common real-world use is deriving handler prop names from a set of event names, so the compiler enforces the naming convention automatically:
type EventName = "click" | "hover" | "focus";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onHover" | "onFocus"
type Handlers = {
[K in HandlerName]: () => void;
};
Capitalize, along with Uncapitalize, Uppercase, and Lowercase, are built-in intrinsic string manipulation types designed specifically to pair with template literals. They cover the transformations you’d otherwise reach for a runtime string method to do, but entirely at the type level.
Pattern matching with infer
Template literal types can also destructure a string type using infer, extracting a piece of it into a new type variable — the type-level equivalent of a regex capture group:
type ExtractRoute<T extends string> =
T extends `/api/${infer Resource}/${infer Id}` ? { resource: Resource; id: Id } : never;
type Parsed = ExtractRoute<"/api/users/42">;
// { resource: "users"; id: "42" }
This is the mechanism behind libraries that type-check route parameters, CSS-in-JS property names, or SQL-like query builders purely from a string literal, without any runtime parsing needed for the types themselves.
Where they fit in the type-system toolbox
Template literal types are most useful alongside mapped types, which they’re frequently used to re-key. Combined with generics, they let a function’s return type depend on the literal string passed in, which is how strongly typed event emitters and query builders are commonly implemented. They also compose with conditional types for the infer-based extraction pattern above, and with discriminated unions when the derived string is used as the discriminant.
When not to use them
Template literal types operate purely on literal string types — they have no effect on plain string, and they don’t validate values you can’t determine at compile time, like user input read from a form or an API response. Reach for utility types or a plain string when the exact contents aren’t knowable ahead of time, and reserve template literal types for cases where the valid values really are a closed, enumerable set: CSS units, route patterns, event names, table or column identifiers.
They can also blow up combinatorially. Interpolating three unions of five values each produces 125 literal types, which is fine for editor tooltips but can slow down tsc on very large unions. If a union of derived strings starts making type-checking noticeably slower, narrowing the source unions or falling back to a plain string with a runtime check is usually the pragmatic fix.
A quick example: typed CSS properties
type Unit = "px" | "rem" | "%";
type CSSLength = `${number}${Unit}`;
function setWidth(el: HTMLElement, width: CSSLength) {
el.style.width = width;
}
setWidth(el, "100%"); // OK
setWidth(el, "100"); // Error: missing a unit
This is a small but representative case: the type system now rejects a bare number that forgot its unit, a mistake that would otherwise silently produce invalid CSS at runtime. If you’re new to the language more broadly, getting started with TypeScript covers the fundamentals this pattern builds on.
The takeaway
Template literal types let TypeScript construct and deconstruct string literal types the same way JavaScript template strings construct runtime strings — interpolating unions to build combinations, extracting substrings with infer, and transforming case with the built-in intrinsic types. They shine for closed sets of string identifiers like CSS units, event names, and route patterns, but they’re the wrong tool for values that genuinely aren’t known until runtime.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.