Bỏ qua để vào nội dung chính
TypeScript Template Literal Types: The Hidden Superpower

TypeScript Template Literal Types: The Hidden Superpower

Bởi Amelia Brooks
21 thg 3, 20266 phút đọc

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 infer for 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.

Không spam, hủy đăng ký bất kỳ lúc nào.

Bài viết liên quan