|$ 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🎯Free Tools
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.

Intrinsic String Manipulation Types

TypeScript provides Uppercase, Lowercase, Capitalize, and Uncapitalize for transforming string literal types — often composed inside mapped key remapping.

untitled.typescript
TypeScript
1type Event = "click" | "scroll";
2type Handler = `on${Capitalize<Event>}`; // "onClick" | "onScroll"
3
4type Props = { name: string; age: number };
5type Getters = {
6 [K in keyof Props as `get${Capitalize<string & K>}`]: () => Props[K];
7};
Pattern Matching with infer
infer-path.ts
TypeScript
1type Parse<S extends string> =
2 S extends `${infer Head}/${infer Tail}`
3 ? [Head, ...Parse<Tail>]
4 : S extends ""
5 ? []
6 : [S];
7
8type Parts = Parse<"a/b/c">; // ["a", "b", "c"]
9
10type Domain<S> = S extends `https://${infer D}/${string}` ? D : never;
11type D = Domain<"https://forgelearn.dev/docs">; // "forgelearn.dev"
CSS-in-TS Units & Colors
untitled.typescript
TypeScript
1type Px = `${number}px`;
2type Rem = `${number}rem`;
3type Length = Px | Rem | 0;
4
5type Hex = `#${string}`;
6type Rgb = `rgb(${number}, ${number}, ${number})`;
7type CssColor = Hex | Rgb | "transparent" | "currentColor";
8
9const ok: Length = "16px";
10const bad: Length = "16em"; // Error if em not in union
Keeping Errors Readable

Deeply nested template literal unions explode error messages. Prefer intermediate named aliases and constrain generics with extends string.

untitled.typescript
TypeScript
1type DotPath<T> = /* complex recursive path union */ string; // hide complexity
2function getByPath<T, P extends DotPath<T>>(obj: T, path: P): unknown {
3 return path.split(".").reduce((a: any, k) => a?.[k], obj);
4}

warning

Recursive template types can hit instantiation depth limits. Add base cases and consider runtime validators for deep paths.
Practice Exercises
  1. Build BEM<B, E, M> producing block__elem--mod literals.
  2. Extract all :param names from a route pattern union.
  3. Map an event name union to an object type of handlers via key remapping.
untitled.typescript
TypeScript
1type BEM<B extends string, E extends string, M extends string> =
2 `${B}__${E}--${M}`;
3type Cls = BEM<"button", "icon", "active">; // "button__icon--active"
Additional Depth — template-literals (1)

Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.

template-literals-0.ts
TypeScript
1// Depth set 1 for template-literals
2type Id0 = string;
3type Box0<T> = { value: T };
4function wrap0<T>(value: T): Box0<T> {
5 return { value };
6}
7const sample0 = wrap0({ ok: true as const, n: 0 });
8type Sample0 = typeof sample0;
9
10type Keys0 = keyof Sample0;
11type Val0 = Sample0["value"];
12
13type Union0 = "a" | "b" | "c";
14type Upper0 = Uppercase<Union0>;
15
16type Fn0 = (x: number) => string;
17type Ret0 = ReturnType<Fn0>;
18type Params0 = Parameters<Fn0>;
19
20type PartialUser0 = Partial<{ id: string; name: string }>;
21type RequiredUser0 = Required<PartialUser0>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — template-literals (2)

Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.

template-literals-1.ts
TypeScript
1// Depth set 2 for template-literals
2type Id1 = string;
3type Box1<T> = { value: T };
4function wrap1<T>(value: T): Box1<T> {
5 return { value };
6}
7const sample1 = wrap1({ ok: true as const, n: 1 });
8type Sample1 = typeof sample1;
9
10type Keys1 = keyof Sample1;
11type Val1 = Sample1["value"];
12
13type Union1 = "a" | "b" | "c";
14type Upper1 = Uppercase<Union1>;
15
16type Fn1 = (x: number) => string;
17type Ret1 = ReturnType<Fn1>;
18type Params1 = Parameters<Fn1>;
19
20type PartialUser1 = Partial<{ id: string; name: string }>;
21type RequiredUser1 = Required<PartialUser1>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — template-literals (3)

Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.

template-literals-2.ts
TypeScript
1// Depth set 3 for template-literals
2type Id2 = string;
3type Box2<T> = { value: T };
4function wrap2<T>(value: T): Box2<T> {
5 return { value };
6}
7const sample2 = wrap2({ ok: true as const, n: 2 });
8type Sample2 = typeof sample2;
9
10type Keys2 = keyof Sample2;
11type Val2 = Sample2["value"];
12
13type Union2 = "a" | "b" | "c";
14type Upper2 = Uppercase<Union2>;
15
16type Fn2 = (x: number) => string;
17type Ret2 = ReturnType<Fn2>;
18type Params2 = Parameters<Fn2>;
19
20type PartialUser2 = Partial<{ id: string; name: string }>;
21type RequiredUser2 = Required<PartialUser2>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — template-literals (4)

Extra worked material to reinforce template-literals. Keep strict: true enabled while experimenting.

template-literals-3.ts
TypeScript
1// Depth set 4 for template-literals
2type Id3 = string;
3type Box3<T> = { value: T };
4function wrap3<T>(value: T): Box3<T> {
5 return { value };
6}
7const sample3 = wrap3({ ok: true as const, n: 3 });
8type Sample3 = typeof sample3;
9
10type Keys3 = keyof Sample3;
11type Val3 = Sample3["value"];
12
13type Union3 = "a" | "b" | "c";
14type Upper3 = Uppercase<Union3>;
15
16type Fn3 = (x: number) => string;
17type Ret3 = ReturnType<Fn3>;
18type Params3 = Parameters<Fn3>;
19
20type PartialUser3 = Partial<{ id: string; name: string }>;
21type RequiredUser3 = Required<PartialUser3>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
$Blueprint — Engineering Documentation·Section ID: TS-TLIT·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.