In the previous tutorial, we learned about mapped types and conditional types. Now let’s learn about template literal types — a way to create type-safe string patterns.

By the end of this tutorial, you will know how to use template literal types, built-in string manipulation types, and practical patterns for events, APIs, and CSS.

What Are Template Literal Types?

Template literal types use the same backtick syntax as JavaScript template literals, but at the type level:

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

const a: Greeting = "Hello, Alex";    // OK
const b: Greeting = "Hello, Sam";     // OK
// const c: Greeting = "Hi, Alex";    // Error: not "Hello, ..."

The ${string} part means “any string.” You can also use other types:

type Port = `${number}`;

const a: Port = "3000";   // OK
const b: Port = "8080";   // OK
// const c: Port = "abc";  // Error: "abc" is not assignable to `${number}`

Combining with Union Types

Template literal types become very powerful when combined with union types. They create every possible combination:

type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

type ClassName = `${Size}-${Color}`;
// Result: "small-red" | "small-blue" | "small-green"
//       | "medium-red" | "medium-blue" | "medium-green"
//       | "large-red" | "large-blue" | "large-green"

TypeScript generates all 9 combinations automatically.

Event Names

type Entity = "user" | "product" | "order";
type Action = "created" | "updated" | "deleted";

type EventName = `${Entity}_${Action}`;
// Result: "user_created" | "user_updated" | "user_deleted"
//       | "product_created" | "product_updated" | "product_deleted"
//       | "order_created" | "order_updated" | "order_deleted"

function emit(event: EventName, data: unknown): void {
  console.log(`Event: ${event}`);
}

emit("user_created", { id: 1 });   // OK
// emit("user_moved", { id: 1 });  // Error: not a valid event

API Routes

type Version = "v1" | "v2";
type Resource = "users" | "products" | "orders";

type APIRoute = `/${Version}/${Resource}`;
// Result: "/v1/users" | "/v1/products" | "/v1/orders"
//       | "/v2/users" | "/v2/products" | "/v2/orders"

Built-in String Manipulation Types

TypeScript provides four built-in types for transforming string types:

Uppercase

type Loud = Uppercase<"hello">;  // "HELLO"

type Status = "active" | "inactive";
type UpperStatus = Uppercase<Status>;  // "ACTIVE" | "INACTIVE"

Lowercase

type Quiet = Lowercase<"HELLO">;  // "hello"

Capitalize

type Cap = Capitalize<"hello">;  // "Hello"

type Event = "click" | "scroll" | "keypress";
type Handler = `on${Capitalize<Event>}`;
// Result: "onClick" | "onScroll" | "onKeypress"

Uncapitalize

type Lower = Uncapitalize<"Hello">;  // "hello"

Real-World Use: Event Handler Names

type DOMEvent = "click" | "focus" | "blur" | "change" | "submit";

type EventHandler = `on${Capitalize<DOMEvent>}`;
// Result: "onClick" | "onFocus" | "onBlur" | "onChange" | "onSubmit"

interface ButtonProps {
  label: string;
  onClick?: () => void;
  onFocus?: () => void;
  onBlur?: () => void;
}

Getter and Setter Types

Combine template literal types with mapped types to create getter/setter patterns:

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 {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// Result: { getName: () => string; getAge: () => number; }

type UserSetters = Setters<User>;
// Result: { setName: (value: string) => void; setAge: (value: number) => void; }

Pattern Matching with Template Literals

You can use template literal types with conditional types to extract parts of strings:

type ExtractId<T> = T extends `user_${infer Id}` ? Id : never;

type A = ExtractId<"user_123">;    // "123"
type B = ExtractId<"user_abc">;    // "abc"
type C = ExtractId<"product_123">; // never — doesn't match

Parsing Event Names

type ParseEvent<T> = T extends `${infer Entity}_${infer Action}`
  ? { entity: Entity; action: Action }
  : never;

type Result = ParseEvent<"user_created">;
// Result: { entity: "user"; action: "created" }

Splitting Paths

type Split<T> = T extends `${infer First}/${infer Rest}`
  ? [First, ...Split<Rest>]
  : [T];

type Path = Split<"api/v1/users">;
// Result: ["api", "v1", "users"]

CSS Property Types

Template literal types work well for CSS-like patterns:

type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;

function setWidth(value: CSSValue): void {
  console.log(`Width: ${value}`);
}

setWidth("100px");   // OK
setWidth("1.5rem");  // OK
setWidth("50%");     // OK
// setWidth("100");  // Error: not a valid CSS value
// setWidth("abc");  // Error

CSS Color Types

type HexDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
  | "a" | "b" | "c" | "d" | "e" | "f";

// Simplified — full hex would be too many combinations
type RGBColor = `rgb(${number}, ${number}, ${number})`;

const color: RGBColor = "rgb(255, 128, 0)"; // OK

Type-Safe String Builders

You can build type-safe query strings:

type QueryParam<K extends string, V extends string | number> = `${K}=${V}`;

type Pagination = QueryParam<"page", number> | QueryParam<"limit", number>;

function buildQuery(params: Pagination[]): string {
  return params.join("&");
}

Type-Safe Object Keys from Strings

Create object types from string patterns:

type Locale = "en" | "de" | "fr";
type TranslationKey = "greeting" | "farewell";

type Translations = Record<Locale, Record<TranslationKey, string>>;

const translations: Translations = {
  en: { greeting: "Hello", farewell: "Goodbye" },
  de: { greeting: "Hallo", farewell: "Tschuess" },
  fr: { greeting: "Bonjour", farewell: "Au revoir" },
};

Performance Considerations

Template literal types with unions create all possible combinations. This can grow very fast:

type A = "a" | "b" | "c" | "d" | "e"; // 5 options
type B = "1" | "2" | "3" | "4" | "5"; // 5 options
type C = "x" | "y" | "z";             // 3 options

type Combined = `${A}-${B}-${C}`; // 5 * 5 * 3 = 75 combinations

If each union has many members, the combinations grow exponentially. TypeScript has a limit (around 100,000 combinations) and will show an error if you exceed it.

Keep your unions small when using template literal types. If you need many combinations, use a simpler type like string with a runtime validation function.

Practical Example: Type-Safe Routes

type Route = "/" | "/about" | "/users" | "/users/:id" | "/products" | "/products/:id";

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

type UserRouteParams = ExtractParam<"/users/:id">; // "id"

function navigate(route: Route): void {
  console.log(`Navigating to ${route}`);
}

navigate("/users");    // OK
navigate("/about");    // OK
// navigate("/blog");  // Error: not a valid route

What’s Next?

In the next tutorial, we will learn about error handling patterns in TypeScript — how to handle errors safely with typed catch blocks, custom error classes, and the Result type pattern.