|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/template-literals
$cat docs/typescript-—-template-literal-types.md
updated Recently·10 min read·published

TypeScript — Template Literal Types

TypeScriptAdvanced
Introduction

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.

Basic Template Literal Types

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.

basic.ts
TypeScript
1// Basic template literal type
2type Greeting = `hello ${string}`;
3
4const valid: Greeting = "hello world"; // OK
5const invalid: Greeting = "hi world"; // Error
6
7// Combining literal and generic types
8type EventName<T extends string> = `on${Capitalize<T>}`;
9
10type ClickEvent = EventName<"click">; // "onClick"
11type FocusEvent = EventName<"focus">; // "onFocus"
12
13// Template literals with multiple interpolations
14type ApiRoute<T extends string, U extends string> = `/api/${T}/${U}`;
15
16type UserPosts = ApiRoute<"users", "posts">; // "/api/users/posts"
17type PostComments = ApiRoute<"posts", "comments">; // "/api/posts/comments"
18
19// Union distribution across template literals
20type Color = "red" | "blue" | "green";
21type Size = "sm" | "md" | "lg";
22
23type ColorSize = `${Color}-${Size}`;
24// "red-sm" | "red-md" | "red-lg" | "blue-sm" | ... | "green-lg"
25
26// Practical: CSS property names
27type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
28type CSSValue = `${number}${CSSUnit}`;
29
30const width: CSSValue = "100px"; // OK
31const height: CSSValue = "50vh"; // OK
32const bad: CSSValue = "100"; // Error
Unions in Template Literals

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.

unions.ts
TypeScript
1// Cartesian product of two unions
2type Horizontal = "left" | "right";
3type Vertical = "top" | "bottom";
4
5type Corner = `${Vertical}-${Horizontal}`;
6// "top-left" | "top-right" | "bottom-left" | "bottom-right"
7
8// Nested unions distribute fully
9type Direction = "n" | "s" | "e" | "w";
10type Distance = "1" | "2" | "3";
11
12type Move = `${Direction}${Distance}`;
13// "n1" | "n2" | "n3" | "s1" | "s2" | "s3" | "e1" | "e2" | "e3" | "w1" | "w2" | "w3"
14
15// Prefix pattern
16type Prefix = "data" | "aria";
17type PropName<T extends string> = `${Prefix}-${T}`;
18
19type DataLabel = PropName<"label">; // "data-label"
20type AriaHidden = PropName<"hidden">; // "aria-hidden"
21
22// Combining with conditional types
23type GetRoutes<T extends string> = T extends `${infer _Start}/${infer _End}`
24 ? "nested"
25 : "flat";
26
27type R1 = GetRoutes<"users/123">; // "nested"
28type R2 = GetRoutes<"health">; // "flat"

best practice

Union distribution in template literals can produce very large types (Cartesian product). Keep each union small — under 20 members. If you have hundreds of combinations, generate them programmatically or use string interpolation with generics instead.
Intrinsic String Types

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.

intrinsic.ts
TypeScript
1// Uppercase — transforms literal to uppercase
2type Upper = Uppercase<"hello">; // "HELLO"
3
4// Lowercase — transforms literal to lowercase
5type Lower = Lowercase<"HELLO">; // "hello"
6
7// Capitalize — capitalizes the first letter
8type Cap = Capitalize<"hello">; // "Hello"
9
10// Uncapitalize — lowercases the first letter
11type Uncap = Uncapitalize<"Hello">; // "hello"
12
13// Combine with template literals for event naming
14type ToEventName<T extends string> = `on${Capitalize<T>}`;
15
16type Click = ToEventName<"click">; // "onClick"
17type Change = ToEventName<"change">; // "onChange"
18
19// Camel case helper
20type CamelCase<S extends string> = S extends `${infer Head}-${infer Tail}`
21 ? `${Head}${CamelCase<Capitalize<Tail>>}`
22 : S;
23
24type KebabToCamel = CamelCase<"background-color">; // "backgroundColor"
25type KebabToCamel2 = CamelCase<"border-top-width">; // "borderTopWidth"
26type KebabToCamel3 = CamelCase<"margin-left">; // "marginLeft"
27
28// Reverse: camel to kebab
29type 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
37type CamelToKebab = KebabCase<"backgroundColor">; // "background-color"
38type CamelToKebab2 = KebabCase<"marginTop">; // "margin-top"

info

The four intrinsic types (Uppercase, Lowercase, Capitalize, Uncapitalize) only work on string literal types, not on the string type itself. Uppercase<string> just resolves to string.
Pattern Matching & Inference

Template literal types support infer for pattern matching on strings. This lets you extract substrings, parse string formats, and build type-safe parsers.

pattern_matching.ts
TypeScript
1// Extract the ID from "/users/:id/posts/:postId"
2type ExtractId<T extends string> =
3 T extends `/users/${infer Id}/posts/${infer PostId}`
4 ? { userId: Id; postId: PostId }
5 : never;
6
7type Result = ExtractId<"/users/42/posts/99">;
8// { userId: "42"; postId: "99" }
9
10// Match a specific prefix
11type HasPrefix<T extends string> =
12 T extends `api-${infer _Rest}` ? true : false;
13
14type A = HasPrefix<"api-users">; // true
15type B = HasPrefix<"web-users">; // false
16
17// Extract path segments
18type Split<S extends string, D extends string> =
19 S extends `${infer Head}${D}${infer Tail}`
20 ? [Head, ...Split<Tail, D>]
21 : [S];
22
23type Segments = Split<"a/b/c", "/">; // ["a", "b", "c"]
24
25// Parse query parameters from a string
26type 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
33type Params = ParseQuery<"name=alice&age=30">;
34// { name: "alice" } & { age: "30" }
35
36// Validate email-like pattern
37type IsEmail<T extends string> =
38 T extends `${infer User}@${infer Domain}.${infer TLD}`
39 ? true
40 : false;
41
42type E1 = IsEmail<"user@example.com">; // true
43type E2 = IsEmail<"not-an-email">; // false
Real-World Patterns

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.

real_world.ts
TypeScript
1// 1. CSS prop builder — type-safe margin/padding props
2type Direction = "top" | "right" | "bottom" | "left";
3type Spacing = "0" | "1" | "2" | "3" | "4" | "6" | "8";
4
5type MarginProps = {
6 [K in Direction as `margin-${K}`]: `${Spacing}px` | `${Spacing}rem`;
7};
8
9const 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
17type DOMEvent = "click" | "change" | "submit" | "keydown" | "focus";
18type EventHandlerProps = {
19 [E in DOMEvent as `on${Capitalize<E>}`]: (e: Event) => void;
20};
21
22const 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
31type Method = "GET" | "POST" | "PUT" | "DELETE";
32type Resource = "users" | "posts" | "comments";
33
34type Endpoint = `/api/${Lowercase<Resource>}`;
35type TypedFetch<T extends Endpoint> = {
36 url: T;
37 method: Method;
38};
39
40const req: TypedFetch<"/api/users"> = {
41 url: "/api/users",
42 method: "GET",
43};
44
45// 4. Configuration key path access
46type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
47type DotJoin<A extends string, B extends string> =
48 `${A}${DotPrefix<B>}`;
49
50type Config = {
51 database: { host: string; port: number };
52 cache: { ttl: number };
53};
54
55type 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
62type Paths = ConfigPath<"", Config>;
63// ".database" | ".database.host" | ".database.port" | ".cache" | ".cache.ttl"

best practice

Use template literal types to enforce naming conventions at compile time. Instead of runtime string manipulation and validation, make invalid states unrepresentable in your type definitions.
Best Practices

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.

$Blueprint — Engineering Documentation·Section ID: TS-TLIT·Revision: 1.0