Suggest an editImprove this articleRefine the answer for “Template literal types in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Template literal types** in TypeScript generate new string literal types using template syntax, producing every union combination at compile time. `${Size}-${Color}` with `"small" | "large"` and `"red" | "blue"` gives exactly `"small-red" | "small-blue" | "large-red" | "large-blue"`. **Key point:** the expansion is distributive and happens only in the type checker, with zero runtime cost.Shown above the full answer for quick recall.Answer (EN)Image**Template literal types** let TypeScript generate new string literal types using template syntax at compile time, producing every combination of the provided unions automatically. ## Theory ### TL;DR - Like a Mad Libs: you define the blank options (unions), TypeScript generates every possible filled sentence as a distinct type. - The core behavior is distributive: `` `${A | B}-${C}` `` expands to `"A-C" | "B-C"`, a cartesian product. - Zero runtime cost. Types erase to `string`; the expansion happens only in the type checker. - Use when you need exhaustive, type-safe string keys or paths. Skip for simple strings or runtime generation. - Added in TypeScript 4.1, March 2021. ### Quick example ```typescript type Size = "small" | "large"; type Color = "red" | "blue"; type TShirt = `${Size}-${Color}`; // "small-red" | "small-blue" | "large-red" | "large-blue" const shirt: TShirt = "small-red"; // ✅ const invalid: TShirt = "small"; // ❌ Type '"small"' is not assignable to type 'TShirt' ``` TypeScript takes the two unions, applies the template to each member combination, and produces four exact string literals. You never list them manually. ### Key difference from plain unions Plain string unions are static: you write every member by hand. Template literal types generate the full set from parts. Three sizes times three colors gives nine strings, but you only define six values. That cartesian expansion is the whole point. Also, template literal types check *format*, not just value. `` `user/${number}/profile` `` accepts `"user/123/profile"` but rejects `"user/abc/profile"`. Plain unions cannot express that constraint. ### When to use - **Generated key sets** (event names, CSS class names, getter/setter pairs): use template literals to avoid manual listing. - **API route patterns** with dynamic segments: template literals enforce the format at compile time. - **Type-safe event emitters** where method names follow a convention like `onEventName`. - **Simple static strings** with fewer than 5 options and no pattern: plain string literals are fine. - **Runtime string construction**: use regular template strings. Template literal types are compile-time only. ### How the compiler handles this TypeScript's type checker (4.1+) parses the template syntax during type checking, recursively substitutes each union member into the placeholder, and normalizes the result to a union of string literals. The compiler processes each branch independently. No code runs at runtime. The type erases to `string` in the emitted JavaScript. When a placeholder receives `string` instead of a finite union, the result collapses to a pattern type like `` `${string}-suffix` `` rather than a concrete union. That is by design, not a bug. ### Built-in string manipulation utilities TypeScript ships four intrinsic types that work directly on string literals: ```typescript type Upper = Uppercase<"hello">; // "HELLO" type Lower = Lowercase<"HELLO">; // "hello" type Cap = Capitalize<"hello">; // "Hello" type Uncap = Uncapitalize<"Hello">; // "hello" type Event = "click" | "focus" | "blur"; type Handler = `on${Capitalize<Event>}`; // "onClick" | "onFocus" | "onBlur" ``` These chain left to right and compose with template literal types cleanly. ### Common mistakes **1. Assuming it works at runtime** ```typescript // Runtime function - return type is `string`, not a template literal type const makeKey = (prefix: string) => `key-${prefix}`; type Key = ReturnType<typeof makeKey>; // string // To get the type, define it separately: type Key = `key-${"a" | "b"}`; ``` Template literal types live in the type system. Functions that build strings at runtime return `string`. **2. Mixing `string` into a union** ```typescript type Prefix = "a" | "b"; type T = `${Prefix | string}-suffix`; // `${string}-suffix`, not "a-suffix" | "b-suffix" ``` `string` absorbs the finite union and the concrete literals disappear. Keep placeholders as finite unions. **3. Using `number` as an infinite placeholder** ```typescript type Id = `user-${number}`; // Valid for assignment checks, but produces no concrete union ``` `number` expands to all numeric strings, which TypeScript cannot enumerate. The type accepts `"user-123"` but gives no autocomplete or exhaustiveness checking. Use a finite union of numbers or a branded type if you need those. **4. Deep recursion hitting the limit** Recursive template literal types hit "Type instantiation is excessively deep and possibly infinite" at around 10-20 levels. Flatten the recursion or use mapped types with a known depth if you run into this. ### Real-world usage - **React Router v6 style**: `` `user/${number}/${"view" | "edit" | "delete"}` `` for type-safe navigation. - **TanStack Query**: key types like `` `user-${number}-profile` `` to avoid key collisions. - **tRPC**: procedure path types built from the router shape. - **GraphQL Codegen**: resolver types as `` `Query.${Keys}` ``. - **Tailwind-like utilities**: `` `p${Direction}-${Spacing}` `` for padding class names. - **Mapped types with key remapping**: `` `get${Capitalize<string & K>}` `` to generate getter names from an interface. ### Follow-up questions **Q:** What does "distributive" mean here? Give an example. **A:** TypeScript applies the template independently to each union member. `` `${"a" | "b"}-${"x" | "y"}` `` becomes `"a-x" | "a-y" | "b-x" | "b-y"`. It is a cartesian product, not a zip. **Q:** How do `Uppercase`, `Capitalize`, and similar utilities interact with template literal types? **A:** They chain cleanly. `` `on${Capitalize<"click" | "focus">}` `` gives `"onClick" | "onFocus"`. They resolve left to right inside the template. **Q:** What is the difference between template literal types and `as const`? **A:** `as const` narrows an existing value to its literal type. Template literal types *generate new* string literal types from parts. They solve different problems and can be combined. **Q:** How deep can recursion go? **A:** Roughly 10-20 levels before TypeScript throws "Type instantiation is excessively deep and possibly infinite". Use mapped types or limit recursion depth explicitly for complex path types. **Q:** (Senior) Write a type that extracts the prefix from a `` `${Prefix}-${Suffix}` `` pattern. **A:** ```typescript type ExtractPrefix<T extends `${string}-${string}`> = T extends `${infer P}-${string}` ? P : never; type Result = ExtractPrefix<"small-red">; // "small" ``` `infer` inside a template literal type captures the matched segment as a new type variable. ## Examples ### Basic: CSS utility class names ```typescript type Spacing = 0 | 1 | 2 | 4 | 8; type Direction = "t" | "b" | "l" | "r"; type PaddingClass = `p${Direction}-${Spacing}`; // "pt-0" | "pt-1" | "pt-2" | "pt-4" | "pt-8" | "pb-0" | ... 20 combinations const cls: PaddingClass = "pt-4"; // ✅ const bad: PaddingClass = "pt-3"; // ❌ 3 is not in Spacing ``` TypeScript generates all 20 class names from 4 directions and 5 spacing values. Add a new spacing value to the union and every class type updates automatically. ### Intermediate: Type-safe event emitter with mapped types ```typescript type Events = { userCreated: { id: string; name: string }; userDeleted: { id: string }; orderPlaced: { orderId: string; total: number }; }; type EventEmitter = { [K in keyof Events as `on${Capitalize<string & K>}`]: (data: Events[K]) => void; }; // Result: // { // onUserCreated: (data: { id: string; name: string }) => void; // onUserDeleted: (data: { id: string }) => void; // onOrderPlaced: (data: { orderId: string; total: number }) => void; // } ``` Key remapping with `as` inside a mapped type, combined with a template literal and `Capitalize`, is one of the most common patterns in production TypeScript codebases. I use this exact shape in event-driven services to keep the emitter contract in sync with the event map. ### Senior: Extracting route params with recursive infer ```typescript type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractRouteParams<Rest> : T extends `${string}:${infer Param}` ? Param : never; type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId" function navigate<T extends string>( path: T, params: Record<ExtractRouteParams<T>, string> ) { // params is { userId: string; postId: string } for "/users/:userId/posts/:postId" } navigate("/users/:userId/posts/:postId", { userId: "1", postId: "42" }); // ✅ navigate("/users/:userId/posts/:postId", { userId: "1" }); // ❌ Missing postId ``` This is the pattern behind type-safe routers like React Router v6 typed paths. The recursion peels one `:param` at a time and unions the results. It works fine for typical route shapes and only hits the recursion limit at extreme depths.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.