Skip to content
Codeloom
TypeScript

TypeScript Template Literal Types for String Manipulation

Master template literal types to build type-safe string patterns for routes, events, CSS values, and API endpoints in TypeScript.

·6 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Build string pattern types with template literals
  • Use intrinsic string manipulation types like Uppercase and Capitalize
  • Create type-safe event emitters and route handlers

Prerequisites

  • Basic TypeScript generics
  • Union types and literal types

Template literal types, introduced in TypeScript 4.1, let you manipulate strings at the type level. They use the same backtick syntax as JavaScript template literals but operate entirely in the type system. Combined with union distribution and conditional types, they unlock patterns that were previously impossible without code generation.

Basic Syntax

A template literal type interpolates other types into a string pattern.

type Greeting = `Hello, ${string}`;

const a: Greeting = 'Hello, World';   // valid
const b: Greeting = 'Goodbye, World'; // error

You can interpolate unions, and TypeScript distributes across them to produce every combination.

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

This distribution is the key mechanism that makes template literal types powerful.

Intrinsic String Manipulation Types

TypeScript provides four built-in utility types for transforming string literals.

type A = Uppercase<'hello'>;    // 'HELLO'
type B = Lowercase<'HELLO'>;    // 'hello'
type C = Capitalize<'hello'>;   // 'Hello'
type D = Uncapitalize<'Hello'>; // 'hello'

These are compiler intrinsics, not user-defined conditional types. They work on literal string types and distribute over unions.

type Events = 'click' | 'focus' | 'blur';
type HandlerNames = `on${Capitalize<Events>}`;
// 'onClick' | 'onFocus' | 'onBlur'

Type-Safe Event Emitter

Template literal types let you build an event emitter where the handler signature is inferred from the event name.

type EventMap = {
  click: { x: number; y: number };
  focus: { target: string };
  resize: { width: number; height: number };
};

type EventHandler<T> = (payload: T) => void;

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

  on<K extends string & keyof E>(
    event: K,
    handler: EventHandler<E[K]>
  ): void {
    const list = this.handlers.get(event) ?? [];
    list.push(handler);
    this.handlers.set(event, list);
  }

  emit<K extends string & keyof E>(event: K, payload: E[K]): void {
    const list = this.handlers.get(event) ?? [];
    list.forEach((fn) => fn(payload));
  }
}

const emitter = new TypedEmitter<EventMap>();

emitter.on('click', (payload) => {
  // payload is { x: number; y: number }
  console.log(payload.x, payload.y);
});

emitter.on('resize', (payload) => {
  // payload is { width: number; height: number }
  console.log(payload.width);
});

Route Pattern Matching

You can extract dynamic segments from route strings using template literal types and conditional type inference.

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

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

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;
}

// TypeScript enforces that both params are provided
navigate('/users/:userId/posts/:postId', {
  userId: '123',
  postId: '456',
});

CSS Value Types

Template literal types can model CSS values precisely.

type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';
type CSSLength = `${number}${CSSUnit}`;

function setWidth(el: HTMLElement, width: CSSLength) {
  el.style.width = width;
}

setWidth(document.body, '100px');  // valid
setWidth(document.body, '2.5rem'); // valid
setWidth(document.body, 'wide');   // error

You can also model compound CSS values.

type CSSColor = `#${string}` | `rgb(${number}, ${number}, ${number})`;
type Padding = CSSLength | `${CSSLength} ${CSSLength}`;

const p1: Padding = '10px';       // valid
const p2: Padding = '10px 20px';  // valid

Building API Endpoint Types

type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type Endpoint = `/api/${ApiVersion}/${Resource}`;
// '/api/v1/users' | '/api/v1/posts' | '/api/v1/comments'
// | '/api/v2/users' | '/api/v2/posts' | '/api/v2/comments'

async function fetchResource(endpoint: Endpoint): Promise<unknown> {
  const response = await fetch(endpoint);
  return response.json();
}

fetchResource('/api/v1/users');     // valid
fetchResource('/api/v3/users');     // error

Splitting and Joining at the Type Level

You can implement Split and Join as recursive conditional types.

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Tail}`
    ? [Head, ...Split<Tail, D>]
    : [S];

type Parts = Split<'a.b.c', '.'>;
// ['a', 'b', 'c']

type Join<T extends string[], D extends string> =
  T extends [infer Head extends string]
    ? Head
    : T extends [infer Head extends string, ...infer Rest extends string[]]
      ? `${Head}${D}${Join<Rest, D>}`
      : '';

type Joined = Join<['hello', 'world'], '-'>;
// 'hello-world'

Dot-Notation Path Types

A common real-world use is typing deeply nested object access with dot notation.

type Primitive = string | number | boolean | null | undefined;

type PathKeys<T> = T extends Primitive
  ? never
  : {
      [K in keyof T & string]: T[K] extends Primitive
        ? K
        : K | `${K}.${PathKeys<T[K]>}`;
    }[keyof T & string];

interface AppConfig {
  db: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
    enabled: boolean;
  };
}

type ConfigPath = PathKeys<AppConfig>;
// 'db' | 'db.host' | 'db.port' | 'db.credentials'
// | 'db.credentials.user' | 'db.credentials.password'
// | 'cache' | 'cache.ttl' | 'cache.enabled'

function getConfig(path: ConfigPath): unknown {
  // implementation
  return undefined;
}

getConfig('db.host');            // valid
getConfig('db.credentials.user'); // valid
getConfig('db.missing');          // error

Performance Considerations

Template literal types that distribute over large unions can produce a combinatorial explosion. If you have a union with 100 members and combine two of them, TypeScript evaluates 10,000 combinations. Keep your unions small or use branded string types instead when the combination count grows.

// This is fine: 3 x 3 = 9 combinations
type Small = `${'a' | 'b' | 'c'}-${'x' | 'y' | 'z'}`;

// Avoid: large unions produce slow compilation
// type Big = `${HundredStrings}-${HundredStrings}`; // 10,000 types

Wrap Up

Template literal types bring string manipulation into the type system. Use them for event names, route patterns, CSS values, API endpoints, and any string-shaped contract you want the compiler to enforce. Combine them with conditional types and infer for parsing and transformation. Keep union sizes manageable to avoid slow builds.