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.

F

Frontend Interview Team

March 01, 2026

2 min read
Template Literal Types for Safe Routes, Events, and Keys

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 missing

This 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

  1. Q: When do template literal types provide real value?

    • A: When strings follow predictable patterns and typos are common/expensive.
  2. 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.