Template literal types bring JavaScript's backtick strings to the type level. They let you describe strings by shape (#${string}), generate unions of names from other unions (on${Capitalize<Event>}), and even parse strings apart with infer. Together with mapped types they power type-safe CSS helpers, event emitters and router libraries. This lesson shows the syntax, the intrinsic string utilities, and the parsing patterns that make these libraries work.
The syntax mirrors runtime template literals, but the placeholders hold types:
type Lang = "en" | "de";
type Page = "home" | "about";
type RouteKey = `${Lang}/${Page}`;
// "en/home" | "en/about" | "de/home" | "de/about"
type Greeting = `Hello, ${string}!`;
const g: Greeting = "Hello, Ada!"; // OK
const h: Greeting = "Hi, Ada!"; // ErrorWhen a placeholder is a union, TypeScript produces the cross product of all members. Primitive types are allowed as placeholders too: string, number, bigint, boolean, null and undefined. `${number}px` matches "12px" but not "12em" or "px".
Unions are expanded eagerly, and the compiler rejects templates that would exceed 100,000 members. Keep the source unions small.
Four built-in types change the case of string literals. They are implemented inside the compiler and work on unions as well:
| Type | "userName" becomes |
|---|---|
| Uppercase<S> | "USERNAME" |
| Lowercase<S> | "username" |
| Capitalize<S> | "UserName" |
| Uncapitalize<S> | "userName" |
type Field = "name" | "email";
type Setter = `set${Capitalize<Field>}`; // "setName" | "setEmail"The classic use case is generating listener methods from a map of events:
interface Events {
connect: { host: string };
message: { body: string };
close: undefined;
}
type Listeners = {
[E in keyof Events as `on${Capitalize<E>}`]: (payload: Events[E]) => void;
};
// { onConnect: (payload: { host: string }) => void; onMessage: ...; onClose: ... }
function watch<E extends keyof Events>(event: E, handler: (payload: Events[E]) => void) { /* ... */ }
watch("message", (p) => p.body); // p: { body: string }
watch("open", () => {}); // Error: '"open"' is not assignableAdd a new key to Events and every derived name, handler signature and watch overload updates automatically.
Inside a conditional type, infer can capture the part of a string that matches a placeholder. This turns a template into a pattern matcher:
type Split<S extends string, Sep extends string> =
S extends `${infer Head}${Sep}${infer Tail}` ? [Head, ...Split<Tail, Sep>] : [S];
type Parts = Split<"a.b.c", ".">; // ["a", "b", "c"]
type Trim<S extends string> =
S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
type Clean = Trim<" hello ">; // "hello"Each infer placeholder matches the shortest possible string for all but the last position, which is why Split peels off one segment at a time. Matching is by string structure only; there is no regular expression support.
// Strongly typed CSS-like values
type Unit = "px" | "rem" | "%";
type Length = `${number}${Unit}`;
function setWidth(el: HTMLElement, w: Length) { el.style.width = w; }
setWidth(document.body, "100%"); // OK
setWidth(document.body, "100"); // Error
// Object paths for a config accessor
interface Config { db: { host: string; port: number }; debug: boolean }
type Path<T, P extends string = ""> = {
[K in keyof T & string]: T[K] extends object ? Path<T[K], `${P}${K}.`> : `${P}${K}`;
}[keyof T & string];
type ConfigPath = Path<Config>; // "db.host" | "db.port" | "debug"A get(config, "db.port") helper typed with ConfigPath catches typos in dotted paths at compile time, a pattern used by form libraries and i18n tooling.
string value never satisfies `#${string}` unless narrowed or asserted; validate user input at runtime.keyof T & string (or string & K) inside templates so number and symbol keys do not cause errors.What is `` `${"a" | "b"}-${"x" | "y"}` ``?
string, number and other primitives to describe string shapes such as `${number}px`.Uppercase, Lowercase, Capitalize and Uncapitalize transform literal case and work on unions.infer inside a template pattern parses strings apart, enabling typed routes, paths and splitters.Next lesson: Modules and Namespaces — organise code with ES modules, type-only imports and the legacy namespace syntax.