Template Literal Types for Safe Routes, Events, and Keys
Use template literal types to make strings type-safe: route patterns, event names, CSS variables, and i18n keys—with realistic examples.
Frontend Interview Team
March 01, 2026
What you’ll learn
- How template literal types turn “stringly-typed” code into safe APIs
- Practical patterns for routes, event names, and configuration keys
- How to avoid going too far (readability matters)
30‑second interview answer
Template literal types let TypeScript build string unions like `${Entity}:${Action}`. This is useful for turning fragile string conventions (events, routes, feature flags) into compile-time checked APIs. The best usage is when you have a stable naming scheme and want the compiler to prevent typos and invalid combinations.
Pattern 1: Typed event names
Problem
You have a tiny event bus:
type Handler = (payload: unknown) => void;
const handlers = new Map<string, Handler[]>();
export function on(event: string, handler: Handler) {
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
}Typos become runtime bugs.
Solution
type Entity = "user" | "org";
type Action = "created" | "deleted" | "updated";
export type EventName = `${Entity}:${Action}`;
type PayloadByEvent = {
"user:created": { id: string; email: string };
"user:deleted": { id: string };
"user:updated": { id: string; patch: Record<string, unknown> };
"org:created": { id: string; name: string };
"org:deleted": { id: string };
"org:updated": { id: string; patch: Record<string, unknown> };
};
export function on<E extends EventName>(
event: E,
handler: (payload: PayloadByEvent[E]) => void
) {
// store handler (runtime omitted)
}Now you can’t subscribe to a non-existent event.
Pattern 2: Typed route builders
A common frontend need: generate URLs safely.
type RouteName = "home" | "post" | "org";
type RoutePath = {
home: "/";
post: "/blog/:slug";
org: "/org/:orgId";
};
type ParamsFor<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ParamsFor<`/${Rest}`>]: string }
: Path extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
function buildUrl<R extends RouteName>(
route: R,
params: ParamsFor<RoutePath[R]>
): string {
// runtime implementation can replace :param with values
return "" as any;
}
buildUrl("post", { slug: "ts-template-literals" });
buildUrl("home", {});
// buildUrl("org", {}) // type error: orgId missingThis is advanced, but it’s exactly the kind of type-level thinking interviewers like.
Pattern 3: Typed config keys (CSS variables / i18n)
type ThemeToken = "bg" | "text" | "muted";
type CssVar = `--color-${ThemeToken}`;
const vars: Record<CssVar, string> = {
"--color-bg": "#fff",
"--color-text": "#111",
"--color-muted": "#777"
};Production rule of thumb
Use template literal types when:
- You already rely on a naming convention
- Typos are expensive (events, routes, permissions)
- The API is reused across a codebase
Avoid them when:
- The types become unreadable
- The scheme changes frequently
Interview questions
-
Q: When do template literal types provide real value?
- A: When strings follow predictable patterns and typos are common/expensive.
-
Q: What’s the biggest risk?
- A: Over-engineering type logic that becomes hard to maintain.
Quick recap
- Template literal types turn string conventions into compiler-checked APIs.
- Best use cases: events, routes, keys.
- Keep it readable; don’t build a type labyrinth.