In the previous tutorial, we learned about mapped types and conditional types. Now let’s learn about template literal types — a way to create type-safe string patterns.
By the end of this tutorial, you will know how to use template literal types, built-in string manipulation types, and practical patterns for events, APIs, and CSS.
What Are Template Literal Types?
Template literal types use the same backtick syntax as JavaScript template literals, but at the type level:
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, Alex"; // OK
const b: Greeting = "Hello, Sam"; // OK
// const c: Greeting = "Hi, Alex"; // Error: not "Hello, ..."
The ${string} part means “any string.” You can also use other types:
type Port = `${number}`;
const a: Port = "3000"; // OK
const b: Port = "8080"; // OK
// const c: Port = "abc"; // Error: "abc" is not assignable to `${number}`
Combining with Union Types
Template literal types become very powerful when combined with union types. They create every possible combination:
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ClassName = `${Size}-${Color}`;
// Result: "small-red" | "small-blue" | "small-green"
// | "medium-red" | "medium-blue" | "medium-green"
// | "large-red" | "large-blue" | "large-green"
TypeScript generates all 9 combinations automatically.
Event Names
type Entity = "user" | "product" | "order";
type Action = "created" | "updated" | "deleted";
type EventName = `${Entity}_${Action}`;
// Result: "user_created" | "user_updated" | "user_deleted"
// | "product_created" | "product_updated" | "product_deleted"
// | "order_created" | "order_updated" | "order_deleted"
function emit(event: EventName, data: unknown): void {
console.log(`Event: ${event}`);
}
emit("user_created", { id: 1 }); // OK
// emit("user_moved", { id: 1 }); // Error: not a valid event
API Routes
type Version = "v1" | "v2";
type Resource = "users" | "products" | "orders";
type APIRoute = `/${Version}/${Resource}`;
// Result: "/v1/users" | "/v1/products" | "/v1/orders"
// | "/v2/users" | "/v2/products" | "/v2/orders"
Built-in String Manipulation Types
TypeScript provides four built-in types for transforming string types:
Uppercase
type Loud = Uppercase<"hello">; // "HELLO"
type Status = "active" | "inactive";
type UpperStatus = Uppercase<Status>; // "ACTIVE" | "INACTIVE"
Lowercase
type Quiet = Lowercase<"HELLO">; // "hello"
Capitalize
type Cap = Capitalize<"hello">; // "Hello"
type Event = "click" | "scroll" | "keypress";
type Handler = `on${Capitalize<Event>}`;
// Result: "onClick" | "onScroll" | "onKeypress"
Uncapitalize
type Lower = Uncapitalize<"Hello">; // "hello"
Real-World Use: Event Handler Names
type DOMEvent = "click" | "focus" | "blur" | "change" | "submit";
type EventHandler = `on${Capitalize<DOMEvent>}`;
// Result: "onClick" | "onFocus" | "onBlur" | "onChange" | "onSubmit"
interface ButtonProps {
label: string;
onClick?: () => void;
onFocus?: () => void;
onBlur?: () => void;
}
Getter and Setter Types
Combine template literal types with mapped types to create getter/setter patterns:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// Result: { getName: () => string; getAge: () => number; }
type UserSetters = Setters<User>;
// Result: { setName: (value: string) => void; setAge: (value: number) => void; }
Pattern Matching with Template Literals
You can use template literal types with conditional types to extract parts of strings:
type ExtractId<T> = T extends `user_${infer Id}` ? Id : never;
type A = ExtractId<"user_123">; // "123"
type B = ExtractId<"user_abc">; // "abc"
type C = ExtractId<"product_123">; // never — doesn't match
Parsing Event Names
type ParseEvent<T> = T extends `${infer Entity}_${infer Action}`
? { entity: Entity; action: Action }
: never;
type Result = ParseEvent<"user_created">;
// Result: { entity: "user"; action: "created" }
Splitting Paths
type Split<T> = T extends `${infer First}/${infer Rest}`
? [First, ...Split<Rest>]
: [T];
type Path = Split<"api/v1/users">;
// Result: ["api", "v1", "users"]
CSS Property Types
Template literal types work well for CSS-like patterns:
type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue): void {
console.log(`Width: ${value}`);
}
setWidth("100px"); // OK
setWidth("1.5rem"); // OK
setWidth("50%"); // OK
// setWidth("100"); // Error: not a valid CSS value
// setWidth("abc"); // Error
CSS Color Types
type HexDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
| "a" | "b" | "c" | "d" | "e" | "f";
// Simplified — full hex would be too many combinations
type RGBColor = `rgb(${number}, ${number}, ${number})`;
const color: RGBColor = "rgb(255, 128, 0)"; // OK
Type-Safe String Builders
You can build type-safe query strings:
type QueryParam<K extends string, V extends string | number> = `${K}=${V}`;
type Pagination = QueryParam<"page", number> | QueryParam<"limit", number>;
function buildQuery(params: Pagination[]): string {
return params.join("&");
}
Type-Safe Object Keys from Strings
Create object types from string patterns:
type Locale = "en" | "de" | "fr";
type TranslationKey = "greeting" | "farewell";
type Translations = Record<Locale, Record<TranslationKey, string>>;
const translations: Translations = {
en: { greeting: "Hello", farewell: "Goodbye" },
de: { greeting: "Hallo", farewell: "Tschuess" },
fr: { greeting: "Bonjour", farewell: "Au revoir" },
};
Performance Considerations
Template literal types with unions create all possible combinations. This can grow very fast:
type A = "a" | "b" | "c" | "d" | "e"; // 5 options
type B = "1" | "2" | "3" | "4" | "5"; // 5 options
type C = "x" | "y" | "z"; // 3 options
type Combined = `${A}-${B}-${C}`; // 5 * 5 * 3 = 75 combinations
If each union has many members, the combinations grow exponentially. TypeScript has a limit (around 100,000 combinations) and will show an error if you exceed it.
Keep your unions small when using template literal types. If you need many combinations, use a simpler type like string with a runtime validation function.
Practical Example: Type-Safe Routes
type Route = "/" | "/about" | "/users" | "/users/:id" | "/products" | "/products/:id";
type ExtractParam<T> = T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParam<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type UserRouteParams = ExtractParam<"/users/:id">; // "id"
function navigate(route: Route): void {
console.log(`Navigating to ${route}`);
}
navigate("/users"); // OK
navigate("/about"); // OK
// navigate("/blog"); // Error: not a valid route
What’s Next?
In the next tutorial, we will learn about error handling patterns in TypeScript — how to handle errors safely with typed catch blocks, custom error classes, and the Result type pattern.