TypeScript Template Literal Types: The Hidden Superpower
Template literal types let you manipulate strings at the type level. Learn how to build auto-completing route params, CSS utility types, and event handler signatures with zero runtime cost.
Template literal types were introduced in TypeScript 4.1, but most developers still treat them as a curiosity rather than a production tool. That's a mistake. When combined with mapped types and conditional types, template literals let you build APIs that are practically self-documenting — with full autocomplete and zero runtime overhead.
Beyond String Concatenation
At the surface level, template literal types work like JavaScript template strings, but for types:
type Greeting = `Hello, ${string}`;
const valid: Greeting = "Hello, World"; // ✅
const invalid: Greeting = "Hi, World"; // ❌ Type error
// Combine with unions for combinatorial explosion
type Color = "red" | "blue" | "green";
type Shade = "light" | "dark";
type ColorToken = `${Shade}-${Color}`;
// "light-red" | "light-blue" | "light-green" | "dark-red" | "dark-blue" | "dark-green"
// Built-in string manipulation types
type ScreamingColor = Uppercase<Color>; // "RED" | "BLUE" | "GREEN"
type EventName = `on${Capitalize<Color>}Change`;
// "onRedChange" | "onBlueChange" | "onGreenChange"
Notice how TypeScript automatically distributes template literals over unions. A union of 3 × 2 produces all 6 combinations. This combinatorial power is what makes template literals so useful.
Pattern 1: Type-Safe Route Parameters
In a Next.js application, you can extract route parameters directly from the path string at the type level:
// Extract all :param segments from a route string
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type BlogParams = ExtractParams<"/blog/:slug/comments/:commentId">;
// "slug" | "commentId"
// Build a params object type automatically
type RouteParams<T extends string> = {
[K in ExtractParams<T>]: string;
};
function navigate<T extends string>(
route: T,
params: RouteParams<T>
): string {
let result: string = route;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value as string);
}
return result;
}
// Full autocomplete for params!
navigate("/blog/:slug/comments/:commentId", {
slug: "my-post", // ✅ required
commentId: "abc", // ✅ required
});
Pattern 2: CSS Utility Type Generator
Build Tailwind-like utility types that validate class names at compile time:
type SpacingScale = "0" | "1" | "2" | "4" | "8" | "16";
type Direction = "t" | "r" | "b" | "l" | "x" | "y";
type SpacingClass = `p${Direction}-${SpacingScale}` | `m${Direction}-${SpacingScale}`;
// This generates 72 valid class names (2 prefixes × 6 directions × 6 scales)
function spacing(cls: SpacingClass): string {
return cls; // Runtime passthrough, compile-time validation
}
spacing("pt-4"); // ✅
spacing("mx-8"); // ✅
spacing("pz-4"); // ❌ "z" is not a valid direction
Pattern 3: Event Emitter with Auto-Typed Handlers
Create an event system where handler signatures are inferred from event names:
type EventConfig = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"page:view": { path: string; referrer?: string };
};
type EventHandler<K extends keyof EventConfig> = (payload: EventConfig[K]) => void;
class TypedEmitter {
private handlers = new Map<string, Set<Function>>();
on<K extends keyof EventConfig>(event: K, handler: EventHandler<K>): void {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler);
}
emit<K extends keyof EventConfig>(event: K, payload: EventConfig[K]): void {
this.handlers.get(event)?.forEach((fn) => fn(payload));
}
}
const emitter = new TypedEmitter();
emitter.on("user:login", ({ userId, timestamp }) => {
// Both params fully typed ✅
console.log(`User ${userId} logged in at ${timestamp}`);
});
Takeaways
- Template literal types turn string patterns into compile-time contracts
- Use them for route params, CSS utilities, event names, and configuration keys
- Combine with
inferfor recursive string parsing at the type level - Keep the generated union sizes reasonable — TypeScript caps at ~100K members
Template literal types bridge the gap between TypeScript's structural type system and the string-heavy reality of web development. Start small with event names or route params, and you'll quickly find more places where string-level typing eliminates entire categories of bugs.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!