⚡IntermediateAdvanced Type System

Template Literal Types (Advanced)

Build powerful string union types, remap object keys, and use built-in string manipulation types to enforce naming conventions at the type level.

Overview

Template literal types extend the backtick string syntax you know from JavaScript into the **type system**. At the beginner level you saw simple patterns like `/api/${string}`. At the intermediate level, combining template literals with **union distribution**, **key remapping** (`as` in mapped types), and the built-in **string manipulation types** (`Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`) unlocks a whole class of patterns. **Union distribution** means TypeScript automatically expands template literals over union members. The type `on${"click" | "focus"}` (written with backticks in real TS code) produces `"onClick" | "onFocus"` - TypeScript substitutes each union member and merges the results into a new string union. **Key remapping** with `as` inside mapped types lets you rename keys while iterating: the pattern `[K in keyof T as `get${Capitalize<string & K>}`]` transforms any object type into a corresponding getter interface. These patterns let you express naming conventions, event systems, and accessor patterns entirely in types - no runtime code needed.

Key Points

  • •Template literal types use backtick syntax at the type level: `` type R = `/api/${string}` ``
  • •Union distribution: TypeScript expands template literals over each union member automatically
  • •Built-in string manipulation types: `Uppercase<S>`, `Lowercase<S>`, `Capitalize<S>`, `Uncapitalize<S>`
  • •Key remapping: `[K in keyof T as NewKey]` - rename keys while mapping
  • •`string & K` is needed when K is `string | number | symbol` but you need only the string keys
  • •Template literals combine with `infer` to parse string shapes at the type level
  • •Use `never` in key remapping to exclude specific keys from the resulting type

Examples

Union distribution and string manipulation types

type EventName = 'click' | 'focus' | 'blur' | 'change';

// Distribute over union  -  creates "onClick" | "onFocus" | "onBlur" | "onChange"
type HandlerName = `on${Capitalize<EventName>}`;

// Map back to handler function types
type EventHandlers = {
  [E in EventName as `on${Capitalize<E>}`]: (event: Event) => void;
};

// Usage: EventHandlers is { onClick: ...; onFocus: ...; onBlur: ...; onChange: ... }

// HTTP method + resource names
type Method  = 'get' | 'post' | 'put' | 'delete';
type Resource = 'User' | 'Post' | 'Comment';

type ApiMethod = `${Method}${Resource}`;
// "getUser" | "postUser" | ... 12 combinations generated automatically

function callApi(method: ApiMethod): void {
  console.log(`Calling: ${method}`);
}

callApi('getUser');    // OK
callApi('deletePost'); // OK
// callApi('fetchUser'); // Error

Key remapping - generating getter/setter interfaces

interface Person {
  name: string;
  age: number;
  email: string;
}

// Generate getter methods for every property
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

// Generate setter methods for every property
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; getEmail: () => string }

type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; ... }

// Exclude specific keys with never
type OmitById<T> = {
  [K in keyof T as K extends 'id' ? never : K]: T[K];
};

interface User { id: number; name: string; email: string }
type UserWithoutId = OmitById<User>; // { name: string; email: string }

Parsing string patterns with infer

// Extract the resource from an API route string
type ExtractResource<T extends string> =
  T extends `/api/${infer Resource}` ? Resource : never;

type A = ExtractResource<'/api/users'>;    // "users"
type B = ExtractResource<'/api/products'>; // "products"
type C = ExtractResource<'/home'>;         // never

// Parse "on{Event}" handler names back to event names
type ExtractEvent<T extends string> =
  T extends `on${infer E}` ? Uncapitalize<E> : never;

type D = ExtractEvent<'onClick'>;  // "click"
type E = ExtractEvent<'onFocus'>;  // "focus"

// CSS property builder
type Side = 'top' | 'right' | 'bottom' | 'left';
type SpacingProp = 'margin' | 'padding';

type SpacingKey = `${SpacingProp}-${Side}` | SpacingProp;
// "margin" | "padding" | "margin-top" | "padding-left" | ...

type CSSSpacing = Partial<Record<SpacingKey, string | number>>;

const styles: CSSSpacing = {
  margin: '0 auto',
  'padding-top': 16,
  'margin-left': '8px',
};

Common Mistakes

  • `K` in a mapped type is `string | number | symbol` - using it directly in a template literal errors; use `string & K` to narrow to only string keys
  • Over-using template literal types for runtime validation - they only guard types at compile time, not values at runtime; use regex for that
  • Forgetting that very large union distributions (hundreds of combinations) slow TypeScript down - keep combinatorial unions manageable

Note

The `as` clause in a mapped type is where key remapping happens: `[K in keyof T as NewKeyExpression]`. Combined with template literals, this is how you auto-generate APIs like `onPropertyChange` event maps or `getPropertyName` accessor interfaces from any existing type.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What does `` type T = `on${"click" | "focus"}` `` evaluate to?