Template Literal Types in TypeScript
Template literal types construct new string literal types at the type level, just like template literals build strings at runtime. Use them for string pattern matching and type-safe APIs.
A TypeScript template literal type builds a new string literal type using the same syntax as JavaScript template literals, but at the type level. A runtime template literal such as Hello, ${name} produces a string value, while writing that same syntax in a type position produces a string type instead. The example below shows both side by side.
Template literal types were introduced in TypeScript 4.1. They enable type-safe string patterns for event names, message formats, routing, and any API where string structure matters at compile time.
type EventName = `on${string}`;
function listen(event: EventName, handler: () => void) {
// implementation
}
listen("onClick", () => {}); // OK
listen("click", () => {}); // Error
// Output:
// Argument of type '"click"' is not assignable to parameter of type '`on${string}`'.EventName accepts any string that starts with "on". The second call passes "click" without the "on" prefix, and the compiler catches it. This is a simple but practical use: enforce naming conventions at the type level.
How template literal types work
A template literal type looks like a JavaScript template literal but appears in a type position. Placeholders hold other types, and the result is a string literal type.
type World = "world";
type Greeting = `Hello, ${World}`;Greeting resolves to the literal type "Hello, world". The placeholder is replaced with the string literal type "world" at compile time.
Placeholders can contain unions. When a placeholder is a union, the template literal type distributes over each member, producing a union of all combinations.
type Color = "red" | "green" | "blue";
type Shade = "light" | "dark";
type ColorShade = `${Shade}-${Color}`;ColorShade resolves to "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue". Every combination of Shade and Color is produced automatically. Two unions with 2 and 3 members produce a new union with 6 members.
Intrinsic string manipulation types
TypeScript provides four built-in types for transforming string literals: Uppercase, Lowercase, Capitalize, and Uncapitalize. They are intrinsic, meaning they are implemented by the compiler rather than defined in TypeScript code.
| Intrinsic type | Input | Output |
|---|---|---|
| Uppercase | "hello" | "HELLO" |
| Lowercase | "HELLO" | "hello" |
| Capitalize | "hello" | "Hello" |
| Uncapitalize | "Hello" | "hello" |
These are most useful inside template literal types to normalize or transform strings at the type level.
type Method = "get" | "post" | "delete";
type HandlerName = `handle${Capitalize<Method>}`;HandlerName resolves to "handleGet" | "handlePost" | "handleDelete". The Capitalize intrinsic capitalizes each method name before it is interpolated into the template. Without Capitalize, every method name would keep its original lowercase form in the result.
Extracting parts of strings with infer
Template literal types pair powerfully with the infer keyword in conditional types. You can match a string pattern and extract its parts into new type variables.
type GetterName<T> = T extends `get${infer Rest}` ? Rest : never;
type Name1 = GetterName<"getName">;
type Name2 = GetterName<"setName">;Name1 resolves to "Name". Name2 resolves to never because the input string does not start with "get". The infer keyword captures the part of the string after the "get" prefix and binds it to the type variable Rest.
This pattern is useful for parsing structured strings at the type level. A more practical example extracts route parameters:
type RouteParam<T> = T extends `${string}:${infer Param}/${infer Rest}`
? Param | RouteParam<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = RouteParam<"/users/:userId/posts/:postId">;Params resolves to "userId" | "postId". The conditional type recursively matches each colon-prefixed segment in the URL pattern and collects the parameter names into a union. For more on infer, see infer keyword in TypeScript.
Template literal types with mapped types
Template literal types with key remapping in TypeScript enable concise property name transformations. The as clause in a mapped type can use a template literal type to compute new key names.
type WithGetters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Store {
user: { name: string };
posts: { title: string }[];
}
type StoreAccessors = WithGetters<Store>;StoreAccessors resolves to { getUser: () => { name: string }; getPosts: () => { title: string }[] }. Every property in Store gains a corresponding getter method with the "get" prefix and capitalized name.
This pattern scales to any prefix or naming convention. You can generate setters, event handlers, validator types, or any family of related types from a single data interface.
Practical use: type-safe event systems
Template literal types shine in event-driven architectures. You can define event name patterns and enforce them across your codebase.
type Module = "user" | "post" | "comment";
type Action = "created" | "updated" | "deleted";
type AppEvent = `${Module}:${Action}`;
function emit(event: AppEvent, payload: unknown) {
// send event to message bus
}
emit("user:created", { id: 1 }); // OK
emit("user:removed", { id: 1 }); // ErrorAppEvent resolves to a union of 9 specific event name strings, from "user:created" down to "comment:deleted". The compiler rejects "user:removed" because "removed" is not in the Action union. Adding a new action or module automatically generates all the new event name combinations.
Practical use: type-safe CSS class builders
Another common pattern is building CSS class names from a set of modifiers. Template literal types ensure only valid combinations are used.
type Element = "btn" | "card" | "input";
type Variant = "primary" | "secondary" | "danger";
type Size = "sm" | "md" | "lg";
type ClassName = `${Element}--${Variant}-${Size}`;
function className(cls: ClassName): string {
return cls;
}
className("btn--primary-md"); // OK
className("btn--primary-xl"); // ErrorThe compiler rejects "xl" because it is not a valid size. The type system enforces the design system's rules at compile time. This is a lightweight alternative to runtime validation for CSS class generation.
Common mistakes
Expecting template literal types to work at runtime. They are compile-time only. A variable typed with an on-prefixed template literal type is just a plain string at runtime. The type annotation adds a compile-time check, but the emitted JavaScript has no template literal type information.
Using template literal types for unbounded unions. When a placeholder is the general string type, the template literal type resolves to the general string type too, which is not useful for narrowing. Use specific unions in placeholders to get meaningful type narrowing.
Forgetting about union distribution. Two unions in a template literal type multiply to produce every combination. With large unions, the result can become enormous. If Module has 50 members and Action has 20, the combined event type produces 1000 union members. This is fine for the compiler to track, but IDE autocomplete may slow down.
Rune AI
Key Insights
- Template literal types use backtick syntax at the type level: type Greeting =
Hello, ${string}. - Unions in template literal types distribute:
${'a' | 'b'}_idproduces 'a_id' | 'b_id'. - Use infer with template literal types in conditional types to extract parts of string literals.
- The intrinsic types Uppercase, Lowercase, Capitalize, and Uncapitalize transform string literals.
- Template literal types exist only at compile time and do not affect runtime behavior.
Frequently Asked Questions
Can template literal types match strings at runtime?
What are the intrinsic string manipulation types?
Can I use template literal types to build a type-safe routing system?
Conclusion
Template literal types bring string manipulation to the type level. They are the foundation for type-safe event names, CSS class builders, routing systems, and any API where string patterns carry meaning at compile time. Combined with key remapping and mapped types, they enable an entire category of type-level programming that was impossible before TypeScript 4.1.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.