TypeScript — Template Literal Types
Template literal types (TypeScript 4.1+) bring the power of JavaScript template literals to the type system. They let you construct string types from other types, manipulate string unions, and build type-safe APIs that enforce naming conventions, event names, CSS properties, and more.
Template literal types use backtick syntax just like JavaScript template literals, but operate at the type level. You embed types inside ${} expressions to construct new string types.
| 1 | // Basic template literal type |
| 2 | type Greeting = `hello ${string}`; |
| 3 | |
| 4 | const valid: Greeting = "hello world"; // OK |
| 5 | const invalid: Greeting = "hi world"; // Error |
| 6 | |
| 7 | // Combining literal and generic types |
| 8 | type EventName<T extends string> = `on${Capitalize<T>}`; |
| 9 | |
| 10 | type ClickEvent = EventName<"click">; // "onClick" |
| 11 | type FocusEvent = EventName<"focus">; // "onFocus" |
| 12 | |
| 13 | // Template literals with multiple interpolations |
| 14 | type ApiRoute<T extends string, U extends string> = `/api/${T}/${U}`; |
| 15 | |
| 16 | type UserPosts = ApiRoute<"users", "posts">; // "/api/users/posts" |
| 17 | type PostComments = ApiRoute<"posts", "comments">; // "/api/posts/comments" |
| 18 | |
| 19 | // Union distribution across template literals |
| 20 | type Color = "red" | "blue" | "green"; |
| 21 | type Size = "sm" | "md" | "lg"; |
| 22 | |
| 23 | type ColorSize = `${Color}-${Size}`; |
| 24 | // "red-sm" | "red-md" | "red-lg" | "blue-sm" | ... | "green-lg" |
| 25 | |
| 26 | // Practical: CSS property names |
| 27 | type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%"; |
| 28 | type CSSValue = `${number}${CSSUnit}`; |
| 29 | |
| 30 | const width: CSSValue = "100px"; // OK |
| 31 | const height: CSSValue = "50vh"; // OK |
| 32 | const bad: CSSValue = "100"; // Error |
When you interpolate a union type into a template literal, TypeScript distributes over the union, producing the Cartesian product of all combinations. This is one of the most powerful features for building exhaustive string types.
| 1 | // Cartesian product of two unions |
| 2 | type Horizontal = "left" | "right"; |
| 3 | type Vertical = "top" | "bottom"; |
| 4 | |
| 5 | type Corner = `${Vertical}-${Horizontal}`; |
| 6 | // "top-left" | "top-right" | "bottom-left" | "bottom-right" |
| 7 | |
| 8 | // Nested unions distribute fully |
| 9 | type Direction = "n" | "s" | "e" | "w"; |
| 10 | type Distance = "1" | "2" | "3"; |
| 11 | |
| 12 | type Move = `${Direction}${Distance}`; |
| 13 | // "n1" | "n2" | "n3" | "s1" | "s2" | "s3" | "e1" | "e2" | "e3" | "w1" | "w2" | "w3" |
| 14 | |
| 15 | // Prefix pattern |
| 16 | type Prefix = "data" | "aria"; |
| 17 | type PropName<T extends string> = `${Prefix}-${T}`; |
| 18 | |
| 19 | type DataLabel = PropName<"label">; // "data-label" |
| 20 | type AriaHidden = PropName<"hidden">; // "aria-hidden" |
| 21 | |
| 22 | // Combining with conditional types |
| 23 | type GetRoutes<T extends string> = T extends `${infer _Start}/${infer _End}` |
| 24 | ? "nested" |
| 25 | : "flat"; |
| 26 | |
| 27 | type R1 = GetRoutes<"users/123">; // "nested" |
| 28 | type R2 = GetRoutes<"health">; // "flat" |
best practice
TypeScript provides four built-in intrinsic types for string manipulation. These operate on individual string literal types and are essential for building ergonomic template literal type patterns.
| 1 | // Uppercase — transforms literal to uppercase |
| 2 | type Upper = Uppercase<"hello">; // "HELLO" |
| 3 | |
| 4 | // Lowercase — transforms literal to lowercase |
| 5 | type Lower = Lowercase<"HELLO">; // "hello" |
| 6 | |
| 7 | // Capitalize — capitalizes the first letter |
| 8 | type Cap = Capitalize<"hello">; // "Hello" |
| 9 | |
| 10 | // Uncapitalize — lowercases the first letter |
| 11 | type Uncap = Uncapitalize<"Hello">; // "hello" |
| 12 | |
| 13 | // Combine with template literals for event naming |
| 14 | type ToEventName<T extends string> = `on${Capitalize<T>}`; |
| 15 | |
| 16 | type Click = ToEventName<"click">; // "onClick" |
| 17 | type Change = ToEventName<"change">; // "onChange" |
| 18 | |
| 19 | // Camel case helper |
| 20 | type CamelCase<S extends string> = S extends `${infer Head}-${infer Tail}` |
| 21 | ? `${Head}${CamelCase<Capitalize<Tail>>}` |
| 22 | : S; |
| 23 | |
| 24 | type KebabToCamel = CamelCase<"background-color">; // "backgroundColor" |
| 25 | type KebabToCamel2 = CamelCase<"border-top-width">; // "borderTopWidth" |
| 26 | type KebabToCamel3 = CamelCase<"margin-left">; // "marginLeft" |
| 27 | |
| 28 | // Reverse: camel to kebab |
| 29 | type KebabCase<S extends string> = S extends `${infer Head}${infer Tail}` |
| 30 | ? Head extends Uppercase<Head> |
| 31 | ? Head extends Lowercase<Head> |
| 32 | ? `${Head}${KebabCase<Tail>}` |
| 33 | : `-${Lowercase<Head>}${KebabCase<Tail>}` |
| 34 | : `${Head}${KebabCase<Tail>}` |
| 35 | : S; |
| 36 | |
| 37 | type CamelToKebab = KebabCase<"backgroundColor">; // "background-color" |
| 38 | type CamelToKebab2 = KebabCase<"marginTop">; // "margin-top" |
info
Template literal types support infer for pattern matching on strings. This lets you extract substrings, parse string formats, and build type-safe parsers.
| 1 | // Extract the ID from "/users/:id/posts/:postId" |
| 2 | type ExtractId<T extends string> = |
| 3 | T extends `/users/${infer Id}/posts/${infer PostId}` |
| 4 | ? { userId: Id; postId: PostId } |
| 5 | : never; |
| 6 | |
| 7 | type Result = ExtractId<"/users/42/posts/99">; |
| 8 | // { userId: "42"; postId: "99" } |
| 9 | |
| 10 | // Match a specific prefix |
| 11 | type HasPrefix<T extends string> = |
| 12 | T extends `api-${infer _Rest}` ? true : false; |
| 13 | |
| 14 | type A = HasPrefix<"api-users">; // true |
| 15 | type B = HasPrefix<"web-users">; // false |
| 16 | |
| 17 | // Extract path segments |
| 18 | type Split<S extends string, D extends string> = |
| 19 | S extends `${infer Head}${D}${infer Tail}` |
| 20 | ? [Head, ...Split<Tail, D>] |
| 21 | : [S]; |
| 22 | |
| 23 | type Segments = Split<"a/b/c", "/">; // ["a", "b", "c"] |
| 24 | |
| 25 | // Parse query parameters from a string |
| 26 | type ParseQuery<T extends string> = |
| 27 | T extends `${infer Key}=${infer Value}&${infer Rest}` |
| 28 | ? { [K in Key]: Value } & ParseQuery<Rest> |
| 29 | : T extends `${infer Key}=${infer Value}` |
| 30 | ? { [K in Key]: Value } |
| 31 | : {}; |
| 32 | |
| 33 | type Params = ParseQuery<"name=alice&age=30">; |
| 34 | // { name: "alice" } & { age: "30" } |
| 35 | |
| 36 | // Validate email-like pattern |
| 37 | type IsEmail<T extends string> = |
| 38 | T extends `${infer User}@${infer Domain}.${infer TLD}` |
| 39 | ? true |
| 40 | : false; |
| 41 | |
| 42 | type E1 = IsEmail<"user@example.com">; // true |
| 43 | type E2 = IsEmail<"not-an-email">; // false |
Template literal types shine in real-world APIs where string shapes matter: CSS-in-JS prop builders, event handler registries, typed API route definitions, and configuration key access.
| 1 | // 1. CSS prop builder — type-safe margin/padding props |
| 2 | type Direction = "top" | "right" | "bottom" | "left"; |
| 3 | type Spacing = "0" | "1" | "2" | "3" | "4" | "6" | "8"; |
| 4 | |
| 5 | type MarginProps = { |
| 6 | [K in Direction as `margin-${K}`]: `${Spacing}px` | `${Spacing}rem`; |
| 7 | }; |
| 8 | |
| 9 | const m: MarginProps = { |
| 10 | "margin-top": "4px", // OK |
| 11 | "margin-left": "8rem", // OK |
| 12 | "margin-bottom": "2px", // OK |
| 13 | "margin-right": "0px", // OK |
| 14 | }; |
| 15 | |
| 16 | // 2. Typed event handlers |
| 17 | type DOMEvent = "click" | "change" | "submit" | "keydown" | "focus"; |
| 18 | type EventHandlerProps = { |
| 19 | [E in DOMEvent as `on${Capitalize<E>}`]: (e: Event) => void; |
| 20 | }; |
| 21 | |
| 22 | const handlers: EventHandlerProps = { |
| 23 | onClick: (e) => console.log("clicked"), |
| 24 | onChange: (e) => console.log("changed"), |
| 25 | onSubmit: (e) => console.log("submitted"), |
| 26 | onKeydown: (e) => console.log("key pressed"), |
| 27 | onFocus: (e) => console.log("focused"), |
| 28 | }; |
| 29 | |
| 30 | // 3. API endpoint type builder |
| 31 | type Method = "GET" | "POST" | "PUT" | "DELETE"; |
| 32 | type Resource = "users" | "posts" | "comments"; |
| 33 | |
| 34 | type Endpoint = `/api/${Lowercase<Resource>}`; |
| 35 | type TypedFetch<T extends Endpoint> = { |
| 36 | url: T; |
| 37 | method: Method; |
| 38 | }; |
| 39 | |
| 40 | const req: TypedFetch<"/api/users"> = { |
| 41 | url: "/api/users", |
| 42 | method: "GET", |
| 43 | }; |
| 44 | |
| 45 | // 4. Configuration key path access |
| 46 | type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`; |
| 47 | type DotJoin<A extends string, B extends string> = |
| 48 | `${A}${DotPrefix<B>}`; |
| 49 | |
| 50 | type Config = { |
| 51 | database: { host: string; port: number }; |
| 52 | cache: { ttl: number }; |
| 53 | }; |
| 54 | |
| 55 | type ConfigPath<K extends string, V> = |
| 56 | V extends object |
| 57 | ? { [P in keyof V & string]: DotJoin<K, P> } & { |
| 58 | [P in keyof V & string]: ConfigPath<DotJoin<K, P>, V[P]>; |
| 59 | }[keyof V & string] |
| 60 | : K; |
| 61 | |
| 62 | type Paths = ConfigPath<"", Config>; |
| 63 | // ".database" | ".database.host" | ".database.port" | ".cache" | ".cache.ttl" |
best practice
Template literal types are powerful but can quickly become unwieldy. Follow these guidelines to keep your types maintainable and your editor performant.
Keep unions small. Cartesian products grow fast — 5 × 5 × 5 = 125 types. If you exceed ~50 combinations, reconsider the design or use generic string interpolation.
Use intrinsic types for casing. Capitalize and Uncapitalize are faster and clearer than manual case conversion with conditional types.
Prefer constraints over unions. Use ${infer} and conditional types for extraction, not just union matching. This works with any string, not just known values.
Name your types clearly. Recursive template literal types are hard to debug. Extract intermediate types with meaningful names so errors are readable.
Beware circular types. Recursive template literal types can cause infinite instantiation errors. Always ensure the recursion terminates with a base case.
TypeScript provides Uppercase, Lowercase, Capitalize, and Uncapitalize for transforming string literal types — often composed inside mapped key remapping.
| 1 | type Event = "click" | "scroll"; |
| 2 | type Handler = `on${Capitalize<Event>}`; // "onClick" | "onScroll" |
| 3 | |
| 4 | type Props = { name: string; age: number }; |
| 5 | type Getters = { |
| 6 | [K in keyof Props as `get${Capitalize<string & K>}`]: () => Props[K]; |
| 7 | }; |
| 1 | type Parse<S extends string> = |
| 2 | S extends `${infer Head}/${infer Tail}` |
| 3 | ? [Head, ...Parse<Tail>] |
| 4 | : S extends "" |
| 5 | ? [] |
| 6 | : [S]; |
| 7 | |
| 8 | type Parts = Parse<"a/b/c">; // ["a", "b", "c"] |
| 9 | |
| 10 | type Domain<S> = S extends `https://${infer D}/${string}` ? D : never; |
| 11 | type D = Domain<"https://forgelearn.dev/docs">; // "forgelearn.dev" |
| 1 | type Px = `${number}px`; |
| 2 | type Rem = `${number}rem`; |
| 3 | type Length = Px | Rem | 0; |
| 4 | |
| 5 | type Hex = `#${string}`; |
| 6 | type Rgb = `rgb(${number}, ${number}, ${number})`; |
| 7 | type CssColor = Hex | Rgb | "transparent" | "currentColor"; |
| 8 | |
| 9 | const ok: Length = "16px"; |
| 10 | const bad: Length = "16em"; // Error if em not in union |
Deeply nested template literal unions explode error messages. Prefer intermediate named aliases and constrain generics with extends string.
| 1 | type DotPath<T> = /* complex recursive path union */ string; // hide complexity |
| 2 | function getByPath<T, P extends DotPath<T>>(obj: T, path: P): unknown { |
| 3 | return path.split(".").reduce((a: any, k) => a?.[k], obj); |
| 4 | } |
warning
- Build BEM<B, E, M> producing block__elem--mod literals.
- Extract all :param names from a route pattern union.
- Map an event name union to an object type of handlers via key remapping.
| 1 | type BEM<B extends string, E extends string, M extends string> = |
| 2 | `${B}__${E}--${M}`; |
| 3 | type Cls = BEM<"button", "icon", "active">; // "button__icon--active" |
Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.
| 1 | // Depth set 1 for template-literals |
| 2 | type Id0 = string; |
| 3 | type Box0<T> = { value: T }; |
| 4 | function wrap0<T>(value: T): Box0<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample0 = wrap0({ ok: true as const, n: 0 }); |
| 8 | type Sample0 = typeof sample0; |
| 9 | |
| 10 | type Keys0 = keyof Sample0; |
| 11 | type Val0 = Sample0["value"]; |
| 12 | |
| 13 | type Union0 = "a" | "b" | "c"; |
| 14 | type Upper0 = Uppercase<Union0>; |
| 15 | |
| 16 | type Fn0 = (x: number) => string; |
| 17 | type Ret0 = ReturnType<Fn0>; |
| 18 | type Params0 = Parameters<Fn0>; |
| 19 | |
| 20 | type PartialUser0 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser0 = Required<PartialUser0>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.
| 1 | // Depth set 2 for template-literals |
| 2 | type Id1 = string; |
| 3 | type Box1<T> = { value: T }; |
| 4 | function wrap1<T>(value: T): Box1<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample1 = wrap1({ ok: true as const, n: 1 }); |
| 8 | type Sample1 = typeof sample1; |
| 9 | |
| 10 | type Keys1 = keyof Sample1; |
| 11 | type Val1 = Sample1["value"]; |
| 12 | |
| 13 | type Union1 = "a" | "b" | "c"; |
| 14 | type Upper1 = Uppercase<Union1>; |
| 15 | |
| 16 | type Fn1 = (x: number) => string; |
| 17 | type Ret1 = ReturnType<Fn1>; |
| 18 | type Params1 = Parameters<Fn1>; |
| 19 | |
| 20 | type PartialUser1 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser1 = Required<PartialUser1>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.
| 1 | // Depth set 3 for template-literals |
| 2 | type Id2 = string; |
| 3 | type Box2<T> = { value: T }; |
| 4 | function wrap2<T>(value: T): Box2<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample2 = wrap2({ ok: true as const, n: 2 }); |
| 8 | type Sample2 = typeof sample2; |
| 9 | |
| 10 | type Keys2 = keyof Sample2; |
| 11 | type Val2 = Sample2["value"]; |
| 12 | |
| 13 | type Union2 = "a" | "b" | "c"; |
| 14 | type Upper2 = Uppercase<Union2>; |
| 15 | |
| 16 | type Fn2 = (x: number) => string; |
| 17 | type Ret2 = ReturnType<Fn2>; |
| 18 | type Params2 = Parameters<Fn2>; |
| 19 | |
| 20 | type PartialUser2 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser2 = Required<PartialUser2>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.
| 1 | // Depth set 4 for template-literals |
| 2 | type Id3 = string; |
| 3 | type Box3<T> = { value: T }; |
| 4 | function wrap3<T>(value: T): Box3<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample3 = wrap3({ ok: true as const, n: 3 }); |
| 8 | type Sample3 = typeof sample3; |
| 9 | |
| 10 | type Keys3 = keyof Sample3; |
| 11 | type Val3 = Sample3["value"]; |
| 12 | |
| 13 | type Union3 = "a" | "b" | "c"; |
| 14 | type Upper3 = Uppercase<Union3>; |
| 15 | |
| 16 | type Fn3 = (x: number) => string; |
| 17 | type Ret3 = ReturnType<Fn3>; |
| 18 | type Params3 = Parameters<Fn3>; |
| 19 | |
| 20 | type PartialUser3 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser3 = Required<PartialUser3>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.