TypeScript Template Literal Types

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Template literal types let you use string literals as types and create new string types by combining and transforming existing ones. They enable powerful patterns for APIs and frameworks.

Basic Template Literal Types

// String literal with variables
type EventType = `user:${"created" | "deleted" | "updated"}`;
// "user:created" | "user:deleted" | "user:updated"

type HttpMethod = `${Uppercase<"get" | "post" | "put" | "delete">}`;
// "GET" | "POST" | "PUT" | "DELETE"

// Extract parts from literal
type ExtractUrl<T extends string> = T extends `${infer Protocol}://${infer Host}/${infer Path}`
  ? { protocol: Protocol; host: Host; path: Path }
  : never;

type ParsedUrl = ExtractUrl<"https://example.com/api/users">;
// { protocol: "https"; host: "example.com"; path: "api/users" }

Capitalize and Other String Utilities

// Built-in string manipulation utilities
type Capitalized = Capitalize<"hello">; // "Hello"
type Lowercased = Lowercase<"HELLO">; // "hello"
type Uppercased = Uppercase<"hello">; // "HELLO"
type Uncapitalized = Uncapitalize<"Hello">; // "hello"

// Combine them
type HttpRoute = `/${Uppercase<"api">}/${Lowercase<"USERS">}`;
// "/API/users"

// With unions
type HttpMethods = Uppercase<"get" | "post" | "put">;
// "GET" | "POST" | "PUT"

Real-World Patterns

Event Emitter

// Define event map
interface EventMap {
  "user:created": { userId: number };
  "user:deleted": { userId: number };
  "post:published": { postId: number; authorId: number };
}

// Create handler map from events
type EventHandlers = {
  [E in keyof EventMap]: (payload: EventMap[E]) => void;
};

class TypedEmitter {
  private handlers = new Map<string, Function[]>();

  on<E extends keyof EventMap>(
    event: E,
    handler: (payload: EventMap[E]) => void
  ): void {
    if (!this.handlers.has(String(event))) {
      this.handlers.set(String(event), []);
    }
    this.handlers.get(String(event))!.push(handler);
  }

  emit<E extends keyof EventMap>(event: E, payload: EventMap[E]): void {
    const handlers = this.handlers.get(String(event)) || [];
    handlers.forEach((h) => h(payload));
  }
}

const emitter = new TypedEmitter();
emitter.on("user:created", (payload) => {
  console.log(`User ${payload.userId} created`);
});

emitter.emit("user:created", { userId: 123 });

Data Models with Auto-Generated Methods

// Generate getter and setter names
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserAccessors = Getters<User> & Setters<User>;

class UserModel implements UserAccessors {
  private user: User;

  constructor(user: User) {
    this.user = user;
  }

  getId(): number {
    return this.user.id;
  }

  setId(value: number): void {
    this.user.id = value;
  }

  getName(): string {
    return this.user.name;
  }

  setName(value: string): void {
    this.user.name = value;
  }

  getEmail(): string {
    return this.user.email;
  }

  setEmail(value: string): void {
    this.user.email = value;
  }
}

Route Handlers

// Define routes with template literals
type Routes = {
  "GET /users": { query: { limit?: number } };
  "GET /users/:id": { params: { id: string } };
  "POST /users": { body: Omit<User, "id"> };
  "PUT /users/:id": { params: { id: string }; body: Partial<User> };
  "DELETE /users/:id": { params: { id: string } };
};

// Extract method and path
type ExtractMethod<R extends string> = R extends `${infer M} ${string}`
  ? M extends "GET" | "POST" | "PUT" | "DELETE"
    ? M
    : never
  : never;

type ExtractPath<R extends string> = R extends `${string} ${infer P}`
  ? P
  : never;

type GetRoute = ExtractMethod<"GET /users">; // "GET"
type UserPath = ExtractPath<"GET /users">; // "/users"

// Type-safe router
class Router {
  on<R extends keyof Routes>(
    route: R,
    handler: (input: Routes[R]) => Promise<any>
  ): void {
    // Implementation
  }
}

const router = new Router();
router.on("GET /users", async (input) => {
  console.log(input.query?.limit);
});

router.on("POST /users", async (input) => {
  console.log(input.body.name);
});

Form Field Names

// Generate form field names from object
type FormFields<T> = {
  [K in keyof T as `${string & K}`]: T[K];
};

// Generate HTML input names
type InputNames<T> = {
  [K in keyof T as `input_${string & K}`]: T[K];
};

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

type LoginInputs = InputNames<LoginForm>;
// {
//   input_email: string;
//   input_password: string;
//   input_rememberMe: boolean;
// }

// Generate CSS class names
type CssClasses<T extends Record<string, any>> = {
  [K in keyof T as `.${string & K}`]: T[K];
};

interface Styles {
  primary: { color: "blue" };
  secondary: { color: "gray" };
  danger: { color: "red" };
}

type StyleClasses = CssClasses<Styles>;
// {
//   .primary: { color: "blue" };
//   .secondary: { color: "gray" };
//   .danger: { color: "red" };
// }

GraphQL-Like Queries

// Define resource fields
interface UserResource {
  id: number;
  name: string;
  email: string;
  profile: {
    bio: string;
    avatar: string;
  };
}

// Create field selector type
type QueryFields<T> = {
  [K in keyof T as T[K] extends object
    ? `${string & K}?`
    : string & K]: T[K] extends object
    ? QueryFields<T[K]> | T[K]
    : T[K];
};

// Usage
type UserQuery = QueryFields<UserResource>;
// Can select: id, name, email, profile?, profile.bio, profile.avatar, etc.

// Type-safe field selection
function query<T, F extends keyof T>(
  resource: T,
  fields: F[]
): Pick<T, F> {
  return {} as Pick<T, F>;
}

const user: UserResource = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  profile: { bio: "Developer", avatar: "url" },
};

const selection = query(user, ["id", "name"]);
// selection: { id: number; name: string }

Database Query Builder

// Table schema
interface UserTable {
  id: number;
  name: string;
  email: string;
  created_at: Date;
}

// Query operations
type Where<T> = `WHERE ${keyof T & string} = ?`;
type OrderBy<T> = `ORDER BY ${keyof T & string}`;
type Select<T> = `SELECT ${keyof T & string}`;

// Build query type-safely
type BuildQuery<T, W extends string = "", O extends string = ""> =
  `${Select<T>} FROM table${W extends "" ? "" : ` ${W}`}${O extends "" ? "" : ` ${O}`}`;

type UserQuery1 = BuildQuery<UserTable, Where<UserTable>>;
// `SELECT ... FROM table WHERE id = ? ...`

type UserQuery2 = BuildQuery<UserTable, Where<UserTable>, OrderBy<UserTable>>;
// `SELECT ... FROM table WHERE id = ? ORDER BY ...`

Advanced Patterns

Recursive Template Literals

// Create nested path types
type PathSegments<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T as K extends string
        ? `${Prefix}${Prefix extends "" ? "" : "."}${K}`
        : never]: T[K] extends object
        ? PathSegments<T[K], `${Prefix}${Prefix extends "" ? "" : "."}${K}`> | T[K]
        : T[K];
    }
  : T;

interface DeepConfig {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
}

type AllPaths = PathSegments<DeepConfig>;
// Can access: "server", "server.host", "server.port", "server.ssl", etc.

State Machine Types

// Define state transitions
type States = "idle" | "loading" | "success" | "error";

type ValidTransitions = {
  idle: "loading";
  loading: "success" | "error";
  success: "idle";
  error: "idle";
};

// Create event names from transitions
type Events = {
  [S in States]: {
    [T in ValidTransitions[S] as `${S}_to_${T}`]: {
      from: S;
      to: T;
    };
  };
}[States];

type IdleToLoading = Events & { from: "idle"; to: "loading" };
// Type-safe state machine

Performance Considerations

  1. Avoid complex template literal operations - they can slow compilation
  2. Cache complex types - TypeScript caches computed types
  3. Use sparingly in tight loops - keep templates simple

FAQ

Q: Can I use regex with template literal types? A: No, template literals are for string combinations and extractions. For complex parsing, use helper types with infer.

Q: What's the difference between template literals and string union types? A: Template literals generate combinations. Union types are explicit. Use template literals to avoid repetition.

Q: Do template literal types have runtime impact? A: No, they're compile-time only. Zero runtime cost. They exist purely for type checking.


Template literal types transform TypeScript from a type checker into a code generator at the type level. They enable patterns that feel like magic but are just clever type composition. Use them to create APIs that are impossible to use incorrectly.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro