Skip to main content
Technology & EngineeringTypescript Patterns123 lines

Template Literal Types

Construct and parse string-level types using template literal syntax for compile-time string validation.

Quick Summary25 lines
You are an expert in Template Literal Types for writing type-safe TypeScript.

## Key Points

- Limit the size of unions fed into template literals; the cartesian product can explode and slow the compiler.
- Combine with `infer` to parse strings at the type level, extracting segments for validation or mapping.
- Use the intrinsic manipulation types (`Uppercase`, etc.) to bridge naming conventions between API layers.
- Creating excessively large union types by combining multiple large unions in a single template, which can crash the type checker.
- Forgetting that `string` as an interpolation target matches everything — use it intentionally, not as a fallback.

## Quick Example

```typescript
type Greeting = `Hello, ${string}`;
const valid: Greeting = "Hello, world";  // OK
```

```typescript
type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";
type Token = `${Size}-${Color}`;
// "sm-red" | "sm-blue" | "md-red" | "md-blue" | "lg-red" | "lg-blue"
```
skilldb get typescript-patterns-skills/Template Literal TypesFull skill: 123 lines
Paste into your CLAUDE.md or agent config

Template Literal Types — TypeScript Patterns

You are an expert in Template Literal Types for writing type-safe TypeScript.

Overview

Template literal types let you build new string literal types by interpolating other types inside backtick-delimited type expressions. They enable compile-time validation of string formats such as CSS values, event names, route paths, and environment variable keys. Use them when you need the type system to enforce a string's structure rather than just its value.

Core Philosophy

Template literal types extend TypeScript's type system from validating values to validating the structure of strings themselves. In most type systems, a string is a string — you cannot express at the type level that a CSS color must be "#" followed by six hex characters, or that a route path must contain segments prefixed with ":". Template literal types let you encode these structural constraints so the compiler rejects malformed strings before the code ever runs.

The combination of template literals with infer creates a type-level string parser. You can decompose a string into its constituent parts, extract parameters from URL patterns, and map between naming conventions — all at compile time. This moves validation from runtime assertion functions into the type system, catching entire categories of typos and format errors during development rather than in production.

The trade-off is computational complexity. Template literal types that combine multiple large unions produce a cartesian product of all possible combinations, which can slow the type checker dramatically. The key discipline is to keep the unions small and the templates focused. Use template literal types for well-bounded domains — event names, configuration keys, route patterns — rather than for open-ended string validation that is better handled by runtime parsers.

Core Concepts

Basic interpolation:

type Greeting = `Hello, ${string}`;
const valid: Greeting = "Hello, world";  // OK

Combining unions — the cartesian product:

type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";
type Token = `${Size}-${Color}`;
// "sm-red" | "sm-blue" | "md-red" | "md-blue" | "lg-red" | "lg-blue"

Intrinsic string manipulation types:

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap   = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

Implementation Patterns

Type-safe event emitter:

type EventName = "click" | "scroll" | "resize";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onScroll" | "onResize"

interface Emitter {
  addEventListener<E extends EventName>(
    event: E,
    callback: () => void
  ): void;
}

Parsing dot-notation paths:

type PathKeys<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? PathKeys<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;

interface Config {
  db: { host: string; port: number };
  app: { name: string };
}

type ConfigPaths = PathKeys<Config>;
// "db.host" | "db.port" | "app.name"

Route parameter extraction:

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

Best Practices

  • Limit the size of unions fed into template literals; the cartesian product can explode and slow the compiler.
  • Combine with infer to parse strings at the type level, extracting segments for validation or mapping.
  • Use the intrinsic manipulation types (Uppercase, etc.) to bridge naming conventions between API layers.

Common Pitfalls

  • Creating excessively large union types by combining multiple large unions in a single template, which can crash the type checker.
  • Forgetting that string as an interpolation target matches everything — use it intentionally, not as a fallback.

Anti-Patterns

Combining large unions in a single template. A template like `${A}-${B}-${C}` where each union has 20 members produces 8,000 type variants. This can freeze the type checker and produce incomprehensible error messages. Keep union sizes small or break the template into intermediate types.

Using template literal types for runtime-variable strings. Template literal types validate string literals known at compile time. If the string comes from user input, an API response, or any runtime source typed as string, the template type provides no validation. Use runtime parsing for dynamic strings and reserve template types for static configurations.

Neglecting to test infer-based parsers with edge cases. A type-level string parser that handles "/users/:id" may break on "/users/" (trailing slash, no param) or "/:a/:b/:c" (multiple params). Test your template types with a variety of inputs using type-level assertions to catch regressions.

Overusing intrinsic manipulation types for cosmetic consistency. Converting every string to Uppercase or Capitalize at the type level adds noise when the actual runtime values are lowercase. Only use intrinsic types when the naming convention is genuinely enforced at both the type and runtime levels.

Building template types that are impossible to satisfy. A type like `${number}-${number}-${number}` can match date-like strings but cannot be constructed from a literal without the compiler widening the number to number. Ensure your template types can actually be used by callers without resorting to as casts.

Install this skill directly: skilldb add typescript-patterns-skills

Get CLI access →