TypeScript Template Literal Types — Complete Guide
In this tutorial, you will learn about TypeScript Template Literal Types. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript template literal types let you manipulate string types at compile time — concatenating, capitalizing, and transforming string literals to create precise, type-safe APIs for CSS properties, event names, and more.
What You'll Learn
- Template literal type syntax
- Intrinsic string manipulation types
- Combining with unions for cartesian products
- Real-world patterns: CSS types, event emitters, API routes
Why It Matters
Before template literal types (TS 4.1), string types were static. You could only say "this must be exactly 'hello'". Now you can say "this must start with 'on' followed by a capitalized word" — enabling type-safe event systems, CSS-in-JS, and API route builders without runtime validation.
Real-World Use
The Doda Browser extension API uses template literal types for its event system — "onTabCreated", "onTabRemoved", "onTabUpdated" are derived from a base event name type. This guarantees that addListener("onTabCreated") is valid while addListener("onTabCreatedd") is a compile error.
Learning Path
flowchart LR A[keyof typeof] --> B[Template Literal Types] B --> C[Conditional Types] B --> D[You Are Here] C --> E[Mapped Types] E --> F[Utility Types]
Basic Syntax
type Greeting = `Hello, ${string}!`;
// Any string that starts with "Hello, " and ends with "!"
const msg1: Greeting = "Hello, World!"; // OK
const msg2: Greeting = "Hello, Alice!"; // OK
// const msg3: Greeting = "Hi there!"; // Error
The ${} placeholder accepts any type that extends string | number | bigint | boolean | null | undefined.
String Literal Unions
The real power comes from using union types inside placeholders:
type Size = "small" | "medium" | "large";
type ButtonSize = `btn-${Size}`;
// "btn-small" | "btn-medium" | "btn-large"
const size1: ButtonSize = "btn-small"; // OK
const size2: ButtonSize = "btn-large"; // OK
// const size3: ButtonSize = "btn-xl"; // Error
Cartesian Product of Unions
When you have multiple placeholders with unions, TypeScript produces every combination:
type Color = "red" | "green" | "blue";
type Emphasis = "light" | "dark" | "neutral";
type ThemeColor = `${Emphasis}-${Color}`;
// "light-red" | "light-green" | "light-blue" |
// "dark-red" | "dark-green" | "dark-blue" |
// "neutral-red" | "neutral-green" | "neutral-blue"
// 3 × 3 = 9 string literal types
Intrinsic String Types
TypeScript provides four built-in string manipulation types:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Capital = Capitalize<"hello">; // "Hello"
type Uncapital = Uncapitalize<"Hello">; // "hello"
These work with unions too:
type EventName = "click" | "submit" | "focus";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onSubmit" | "onFocus"
Pattern: Type-Safe Event Emitter
type BaseEvents = "click" | "submit" | "focus" | "blur";
// Generate handler names
type HandlerEvent = `on${Capitalize<BaseEvents>}`;
// "onClick" | "onSubmit" | "onFocus" | "onBlur"
// Generate event handler signatures
type EventHandlers = {
[K in HandlerEvent]: (event: Event) => void;
};
// Use it
class TypedButton implements EventHandlers {
onClick!: (event: Event) => void;
onSubmit!: (event: Event) => void;
onFocus!: (event: Event) => void;
onBlur!: (event: Event) => void;
}
Pattern: CSS Properties
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // OK
const height: CSSValue = "50%"; // OK
// const invalid: CSSValue = "100"; // Error — missing unit
// Partial CSS property type
type CSSProperty = `margin${"" | "Top" | "Right" | "Bottom" | "Left"}`;
// "margin" | "marginTop" | "marginRight" | "marginBottom" | "marginLeft"
type CSSWithValue = {
[K in CSSProperty]?: CSSValue;
};
const style: CSSWithValue = {
margin: "10px",
marginTop: "20px",
marginLeft: "auto",
};
Pattern: API Route Builder
type Resource = "users" | "scans" | "threats" | "reports";
type Action = "list" | "get" | "create" | "update" | "delete";
type APIRoute = `/${Resource}`;
// "/users" | "/scans" | "/threats" | "/reports"
type APIRouteWithId = `/${Resource}/${number}`;
// "/users/123" | "/scans/123" | ...
type APIMethod = {
[R in Resource]: {
[A in Action]: string;
};
};
type MethodMap = {
[R in Resource as `/${R}`]: {
[A in Action]: `/${R}/${A}`;
};
};
Pattern: Formatted Logging
type LogLevel = "info" | "warn" | "error";
type LogMessage = `[${Uppercase<LogLevel>}] ${string}`;
const msg1: LogMessage = "[INFO] Server started"; // OK
const msg2: LogMessage = "[WARN] Disk space low"; // OK
const msg3: LogMessage = "[ERROR] Connection failed"; // OK
// const msg4: LogMessage = "Server started"; // Error
Advanced: Inferring from Template Literals
Combine with infer to extract parts of strings:
type ExtractId<T extends string> =
T extends `/api/${infer Resource}/${infer Id}`
? { resource: Resource; id: Id }
: never;
type Result1 = ExtractId<"/api/users/123">;
// { resource: "users"; id: "123" }
type Result2 = ExtractId<"/api/scans/scan-001">;
// { resource: "scans"; id: "scan-001" }
Common Mistakes
1. Forgetting That Template Literal Types Only Work with String/Number Literals
type Test<T extends string> = `hello-${T}`;
type Result = Test<string>; // string — too wide, no specific literals
Template literals need literal types (unions of literals) to produce useful results.
2. Creating Too Many Combinations
type A = "a" | "b" | "c";
type B = "d" | "e" | "f";
type Three = `${A}-${B}`; // 9 combinations — manageable
type Many = `${A}-${B}-${A}-${B}`; // 81 combinations — getting large
Very large template literal unions can slow down Type Checking.
3. Confusing Capitalize with Uppercase
Capitalize<"hello"> // "Hello" — first letter only
Uppercase<"hello"> // "HELLO" — all letters
4. Using String Concatenation Instead of Template Literals
// Can't use + for types
// type Bad = "on" + "Click"; // Error
// Use template literals
type Good = `on${"Click"}`; // "onClick"
5. Not Handling Empty Strings Properly
type Name = "" | "Alice" | "Bob";
type Greeting = `Hello ${Name}`;
// "Hello " | "Hello Alice" | "Hello Bob"
// Note: "Hello " (with trailing space) is valid!
Practice Questions
What does
${string}mean inside a template literal type? A placeholder that matches any string, making the template match any string with the given prefix and suffix.How do you create a type that matches all possible event handlers for a set of events? Use
type Handler = \on${Capitalize}`` where Event is a union of event name literals. What is the difference between Capitalize and Uppercase? Capitalize uppercases only the first character; Uppercase uppercases all characters.
Can template literal types be used with numbers? Yes:
type Size = `${number}px`;matches strings like "10px", "42px".
Challenge: Create a CSSClass type that represents BEM-style class names: block__element--modifier. Use template literals to validate that a string matches this pattern.
FAQ
Mini Project: Type-Safe CSS Style Builder
// src/css-builder.ts
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw" | "pt";
type CSSValue = `${number}${CSSUnit}`;
type BorderStyle = "solid" | "dashed" | "dotted" | "none";
type ShorthandProperty = "margin" | "padding" | "border";
type Direction = "" | "Top" | "Right" | "Bottom" | "Left";
type CSSProperty = `${ShorthandProperty}${Direction}`;
type BorderProperty = `border${Direction}${"" | "Width" | "Style" | "Color"}`;
type StyleSheet = {
[selector: string]: Partial<{
[K in CSSProperty | BorderProperty]: string;
}>;
};
function createStyle(styles: StyleSheet): string {
let result = "";
for (const [selector, props] of Object.entries(styles)) {
result += `${selector} {\n`;
for (const [prop, value] of Object.entries(props || {})) {
result += ` ${prop}: ${value};\n`;
}
result += "}\n";
}
return result;
}
const styles = createStyle({
".btn": {
margin: "10px",
paddingTop: "5px",
borderBottom: "2px solid red",
},
".btn-primary": {
padding: "8px 16px",
borderStyle: "solid",
marginLeft: "0px",
},
});
console.log(styles);
Expected output:
.btn {
margin: 10px;
paddingTop: 5px;
borderBottom: 2px solid red;
}
.btn-primary {
padding: 8px 16px;
borderStyle: solid;
marginLeft: 0px;
}
What's Next
Now dive deep into conditional types:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/11-keyof-typeof" >}} | Review keyof and typeof |
| {{< ref "/programming-languages/typescript/13-conditional-types" >}} | Conditional types with extends and infer |
| {{< ref "/programming-languages/typescript/14-mapped-types" >}} | Mapped types and type transformations |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro