TypeScript 33 🔷 Template Literal Types
A template literal type is a type built from a string template — the same backtick syntax as JavaScript, but at the type level. It lets you describe strings that match a pattern: \get${string}`, `${number}px`, `${Uppercase}Id`. Combined with unions and infer`, template literal types can split strings, generate combinations, rename keys, and validate formats. They’re how libraries type route paths, event names, CSS properties, and DOM APIs. Once you can read them, a whole class of “magic string” types becomes understandable.
Key point: A template literal type uses backticks with ${...} placeholders. The placeholders can be any type that’s assignable to string | number | bigint | boolean | null | undefined. When a placeholder is a union, the result is the union of all combinations. Template literal types compose with mapped types for key remapping, with infer for pattern extraction, and with the standard Uppercase, Lowercase, Capitalize, and Uncapitalize helpers for case conversion.
What a template literal type is
A template literal type is a string type defined by a pattern.
type Greeting = `hello ${string}`;
const a: Greeting = 'hello world'; // ✅
const b: Greeting = 'hello'; // ❌ missing space and word
const c: Greeting = 'hi world'; // ❌ wrong prefix
Greeting matches any string that starts with hello and continues with anything. The ${string} is a placeholder for an arbitrary string.
What’s allowed in placeholders:
| Placeholder type | Example |
|---|---|
string | Any string |
number | Any number |
bigint | Any bigint |
boolean | true or false |
null | null |
undefined | undefined |
| Literal types | 'a', 42, true |
| Unions | 'a' | 'b' |
Everything else — objects, arrays, functions — is rejected. Only types that have a string representation are allowed.
Literal placeholder:
type Event = `on${'Click' | 'Hover'}`;
// 'onClick' | 'onHover'
The union distributes across the template, producing one string per combination.
Multiple placeholders:
type Route = `${'users' | 'posts'}/${number}`;
// 'users/0' | 'users/1' | 'users/2' | ...
// | 'posts/0' | 'posts/1' | ...
Each placeholder can be a union, and the result is the Cartesian product.
Why template literal types exist: They let you describe strings that follow a pattern — not just string. Before them, a route path, event name, or CSS property was either a string (too loose) or a hand-written union (too rigid). Template literal types give you the shape without enumerating every value.
Why “template literal” and not “string pattern”: The syntax mirrors JavaScript template literals — backticks,
${}, interpolation. Using the same syntax means no new grammar to learn. The only difference is that the placeholders hold types, not values.
The case-conversion helpers
TypeScript ships four intrinsic string-type helpers: Uppercase, Lowercase, Capitalize, and Uncapitalize.
type A = Uppercase<'hello'>; // 'HELLO'
type B = Lowercase<'HELLO'>; // 'hello'
type C = Capitalize<'hello'>; // 'Hello'
type D = Uncapitalize<'Hello'>; // 'hello'
Each transforms a string type.
On unions:
type A = Uppercase<'a' | 'b' | 'c'>;
// 'A' | 'B' | 'C'
The helper distributes — each member is transformed separately.
In template literals:
type Getter<K extends string> = `get${Capitalize<K>}`;
type A = Getter<'name'>; // 'getName'
type B = Getter<'email'>; // 'getEmail'
type C = Getter<'name' | 'age'>; // 'getName' | 'getAge'
Capitalize<K> capitalizes the first letter, then the template prepends get. The result is a valid getter name.
On non-string types: The helpers only work on string types. On string (the general type), they return string. On unions of literals, they transform each literal.
type A = Uppercase<string>; // string
type B = Uppercase<'hello'>; // 'HELLO'
type C = Uppercase<string | number>; // string (number unaffected)
Why case helpers matter: They let you generate names. getName from name, USER_ID from userId, userId from UserId. Combined with mapped types, they power the getter/setter generation patterns.
Why the helpers are “intrinsic”: They’re built into the compiler, not implemented in TypeScript. Converting
'hello'to'HELLO'at the type level isn’t expressible with normal type operations — the compiler has to compute it. “Intrinsic” is TypeScript’s term for compiler built-ins. Users can’t define their own.
Union distribution in templates
When a placeholder is a union, the template produces the union of all combinations.
type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue';
type ClassName = `${Size}-${Color}`;
// 'sm-red' | 'sm-blue'
// | 'md-red' | 'md-blue'
// | 'lg-red' | 'lg-blue'
Each combination is a member. The result is the Cartesian product.
Why this is powerful: You write one template, and the type enumerates every valid combination. Adding a new Size member expands the union automatically. No hand-maintained list to keep in sync.
Empty unions: If a placeholder is never, the result is never — no combinations to enumerate.
type A = `prefix-${never}`; // never
Any: If a placeholder is any, the result is string. TypeScript widens because it can’t enumerate combinations.
type A = `prefix-${any}`; // string
string: If a placeholder is string (the wide type), the result is string.
type A = `prefix-${string}`; // `prefix-${string}` (a template type, assignable from any matching string)
Note: \prefix-${string}`is not the same asstring`. It’s a template type — narrower, but still open.
Template plus literal: You can mix literals and placeholders.
type CSS = `${number}px`;
// any string that ends in 'px' and has digits before
type Version = `v${number}.${number}.${number}`;
// 'v1.0.0', 'v12.34.567', etc.
Why distribution matters: Template literal types over unions are the type-level equivalent of generating a list. They’re how libraries enumerate combinations — path parameters, event names, CSS units — without hand-writing every case.
Why the product and not just concatenation: Each placeholder contributes its choices independently. Two placeholders with 3 and 2 members produce 6 combinations. That’s the Cartesian product — and it’s exactly what you want for “all possible combinations of these choices.”
Pattern matching with infer
Template literal types combine with infer to extract parts of strings.
type ExtractPrefix<S> =
S extends `${infer P}-${string}` ? P : never;
type A = ExtractPrefix<'user-123'>; // 'user'
type B = ExtractPrefix<'post-456'>; // 'post'
type C = ExtractPrefix<'no-dash'>; // 'no'
type D = ExtractPrefix<'nothing'>; // never
${infer P}-${string} matches a string with a -, capturing everything before it.
Extracting both parts:
type SplitDash<S> =
S extends `${infer A}-${infer B}` ? [A, B] : never;
type A = SplitDash<'user-123'>; // ['user', '123']
type B = SplitDash<'a-b-c'>; // ['a', 'b-c'] — first dash only
The first infer is greedy — it captures as much as possible, leaving the minimum for the rest. Actually, TypeScript’s behavior here is: the first infer matches as little as possible while letting the rest match. For 'a-b-c', the first - is the split point, so A = 'a' and B = 'b-c'.
Recursive splitting:
type Split<S, Sep extends string> =
S extends `${infer Head}${Sep}${infer Tail}`
? [Head, ...Split<Tail, Sep>]
: [S];
type A = Split<'a.b.c', '.'>; // ['a', 'b', 'c']
type B = Split<'one-two', '-'>; // ['one', 'two']
Recursive infer handles multiple separators.
Extracting a route parameter:
type Param<S> =
S extends `:${infer Name}` ? Name : never;
type A = Param<':id'>; // 'id'
type B = Param<':userId'>; // 'userId'
type C = Param<'literal'>; // never
Inferring a key:
type GetterKey<S> =
S extends `get${infer K}` ? Uncapitalize<K> : never;
type A = GetterKey<'getName'>; // 'name'
type B = GetterKey<'getEmail'>; // 'email'
The get prefix is matched literally, and the rest is captured and uncapitalized.
Why infer with templates matters: It turns a string pattern into a destructuring operation. Split on a separator, extract a prefix, capture a variable name — all expressed in the type system. Combined with distribution, it handles unions of strings.
Why the matching is at the first separator: Template literal inference is greedy from the left — the first
infercaptures as little as possible while still allowing the rest to match. For'a-b-c'with\${infer A}-${infer B}`,Agets‘a’andBgets‘b-c’`. If you want the last separator, you need recursion or a different pattern.
Template literal types with mapped types
The killer combination: mapped types with key remapping via templates.
Generating getters:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }
Each key K becomes get + the capitalized key. The mapped type iterates over the keys and produces the new shape.
Generating setters:
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<User>;
// {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// }
string & K: keyof T can include symbol or number. Template placeholders need string | number | bigint | boolean | null | undefined. string & K narrows K to just strings, satisfying the constraint.
Removing a prefix:
type RemoveGet<T> = {
[K in keyof T as K extends `get${infer R}` ? Uncapitalize<R> : K]: T[K];
};
interface Api {
getName: () => string;
getAge: () => number;
version: string;
}
type Model = RemoveGet<Api>;
// {
// name: () => string;
// age: () => number;
// version: string;
// }
Keys starting with get are renamed to the uncapitalized remainder; other keys are unchanged.
Adding a prefix to all keys:
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Data { a: number; b: string; }
type WithPrefix = Prefixed<Data, 'data'>;
// { dataa: number; datab: string; }
Filtering keys by pattern:
type OnlyGetters<T> = {
[K in keyof T as K extends `get${string}` ? K : never]: T[K];
};
type A = OnlyGetters<Api>;
// { getName: () => string; getAge: () => number; }
Keys not matching the pattern become never and are dropped.
Why this combination matters: Mapped types let you transform every key; templates let you transform the names. Together they generate APIs — getters, setters, event handlers, query builders — from a single source of truth. That’s how libraries like ORMs, HTTP clients, and form builders type their generated methods.
Why
string & Kis needed:Kcan be astring,number, orsymbol. Template literals only accept the first three (and a few more).string & Ksays “the string part of K” — narrowingKto a string literal for the template. It’s a small constraint that unlocks the pattern.
A full example
A type-safe event emitter using template literal types.
// ============================================
// EVENT MAP
// ============================================
interface AppEvents {
userCreated: { id: number; name: string };
userDeleted: { id: number };
orderPlaced: { orderId: string; total: number };
orderShipped: { orderId: string; tracking: string };
}
type EventName = keyof AppEvents;
// ============================================
// HANDLER TYPES
// ============================================
type Handler<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void;
};
type AppHandlers = Handler<AppEvents>;
// {
// onUserCreated: (payload: { id: number; name: string }) => void;
// onUserDeleted: (payload: { id: number }) => void;
// onOrderPlaced: (payload: { orderId: string; total: number }) => void;
// onOrderShipped: (payload: { orderId: string; tracking: string }) => void;
// }
// ============================================
// EVENT EMITTER
// ============================================
class Emitter<T extends Record<string, unknown>> {
private handlers = new Map<keyof T, Function[]>();
on<K extends keyof T>(
event: K,
handler: (payload: T[K]) => void
): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.handlers.get(event)?.forEach(h => h(payload));
}
}
// ============================================
// EVENT NAME PATTERNS
// ============================================
type UserEvent = Extract<EventName, `user${string}`>;
// 'userCreated' | 'userDeleted'
type OrderEvent = Extract<EventName, `order${string}`>;
// 'orderPlaced' | 'orderShipped'
type ActionOf<K extends EventName> =
K extends `${'user' | 'order'}${infer Action}`
? Uncapitalize<Action>
: never;
type A = ActionOf<'userCreated'>; // 'created'
type B = ActionOf<'orderPlaced'>; // 'placed'
// ============================================
// USAGE
// ============================================
const emitter = new Emitter<AppEvents>();
emitter.on('userCreated', payload => {
console.log('created:', payload.name); // payload is { id: number; name: string }
});
emitter.on('orderShipped', payload => {
console.log('shipped:', payload.tracking); // payload is { orderId: string; tracking: string }
});
emitter.emit('userCreated', { id: 1, name: 'Alice' });
emitter.emit('orderShipped', { orderId: 'o-1', tracking: 'TRK123' });
// Wrong payload shape fails
// emitter.emit('userCreated', { id: 1 }); // ❌ missing name
// emitter.emit('userCreated', 'Alice'); // ❌ not an object
// Unknown event name fails
// emitter.on('userUpdated', ...); // ❌ not in AppEvents
// ============================================
// HANDLERS OBJECT
// ============================================
const handlers: Partial<AppHandlers> = {
onUserCreated: payload => console.log(payload.name),
onOrderPlaced: payload => console.log(payload.total)
};
console.log(Object.keys(handlers));
What this shows:
Handler<T>— mapped type withasto rename every key toon${Capitalize<K>}AppHandlers— generated handler methods from the event mapExtract<EventName, \user${string}`>` — filter event names by prefixActionOf<K>— split an event name into the entity and action partsEmitter<T>— a type-safe emitter using the event map
Every operation preserves the types. Adding a new event to AppEvents expands EventName and AppHandlers automatically.
Why this shape: It’s how type-safe event systems are built. The event map is the source of truth. Mapped types generate handler names. Template literals filter and split names. The emitter uses the map for full typing. Adding an event is one line, and everything updates.
Complete Example Session
# ============================================
# PART 1: BASIC TEMPLATE LITERAL
# ============================================
cat > basic.ts << 'EOF'
type Greeting = `hello ${string}`;
const a: Greeting = 'hello world';
// const b: Greeting = 'hi'; // ❌
type Event = `on${'Click' | 'Hover'}`;
const c: Event = 'onClick';
const d: Event = 'onHover';
// const e: Event = 'onOther'; // ❌
console.log(a, c, d);
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: CASE HELPERS
# ============================================
cat > case.ts << 'EOF'
type A = Uppercase<'hello'>; // 'HELLO'
type B = Lowercase<'HELLO'>; // 'hello'
type C = Capitalize<'hello'>; // 'Hello'
type D = Uncapitalize<'Hello'>; // 'hello'
type Getter<K extends string> = `get${Capitalize<K>}`;
type G1 = Getter<'name'>; // 'getName'
type G2 = Getter<'name' | 'age'>; // 'getName' | 'getAge'
const a: A = 'HELLO';
const c: C = 'Hello';
const g1: G1 = 'getName';
const g2: G2 = 'getAge';
console.log(a, c, g1, g2);
EOF
npx tsc --noEmit case.ts
# (no errors)
# ============================================
# PART 3: DISTRIBUTION OVER UNIONS
# ============================================
cat > union.ts << 'EOF'
type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue';
type ClassName = `${Size}-${Color}`;
const a: ClassName = 'sm-red';
const b: ClassName = 'md-blue';
const c: ClassName = 'lg-red';
// const d: ClassName = 'xl-red'; // ❌
console.log(a, b, c);
EOF
npx tsc --noEmit union.ts
# (no errors)
# ============================================
# PART 4: PATTERN MATCHING
# ============================================
cat > pattern.ts << 'EOF'
type SplitDash<S> =
S extends `${infer A}-${infer B}` ? [A, B] : never;
type A = SplitDash<'user-123'>; // ['user', '123']
type B = SplitDash<'a-b-c'>; // ['a', 'b-c']
type GetterKey<S> =
S extends `get${infer K}` ? Uncapitalize<K> : never;
type C = GetterKey<'getName'>; // 'name'
type D = GetterKey<'getEmail'>; // 'email'
const a: A = ['user', '123'];
const b: B = ['a', 'b-c'];
const c: C = 'name';
const d: D = 'email';
console.log(a, b, c, d);
EOF
npx tsc --noEmit pattern.ts
# (no errors)
# ============================================
# PART 5: KEY REMAPPING
# ============================================
cat > remap.ts << 'EOF'
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
const g: UserGetters = {
getName: () => 'Alice',
getAge: () => 30
};
console.log(g.getName());
EOF
npx tsc --noEmit remap.ts
# (no errors)
# ============================================
# PART 6: FILTERING KEYS
# ============================================
cat > filter.ts << 'EOF'
interface Api {
getName: () => string;
getAge: () => number;
version: string;
}
type OnlyGetters<T> = {
[K in keyof T as K extends `get${string}` ? K : never]: T[K];
};
type A = OnlyGetters<Api>;
// { getName: () => string; getAge: () => number }
const a: A = {
getName: () => 'x',
getAge: () => 42
};
console.log(Object.keys(a));
EOF
npx tsc --noEmit filter.ts
# (no errors)
# ============================================
# PART 7: FULL EXAMPLE
# ============================================
cat > events.ts << 'EOF'
interface AppEvents {
userCreated: { id: number; name: string };
orderPlaced: { orderId: string; total: number };
}
type Handler<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void;
};
type AppHandlers = Handler<AppEvents>;
// { onUserCreated: ...; onOrderPlaced: ... }
const handlers: Partial<AppHandlers> = {
onUserCreated: payload => console.log('created:', payload.name),
onOrderPlaced: payload => console.log('placed:', payload.total)
};
handlers.onUserCreated?.({ id: 1, name: 'Alice' });
handlers.onOrderPlaced?.({ orderId: 'o-1', total: 99.99 });
EOF
npx tsc --noEmit events.ts
# (no errors)
# ============================================
# PART 8: COMPILE AND RUN
# ============================================
npx tsc basic.ts case.ts union.ts pattern.ts remap.ts filter.ts events.ts
node basic.js
# [ hello world onClick onHover ]
node case.js
# [ HELLO Hello getName getAge ]
node union.js
# [ sm-red md-blue lg-red ]
node pattern.js
# [ [ 'user', '123' ] [ 'a', 'b-c' ] name email ]
node remap.js
# [ Alice ]
node filter.js
# [ [ 'getName', 'getAge' ] ]
node events.js
# [ created: Alice ]
# [ placed: 99.99 ]
Quick Reference
Template Literal Syntax
| Form | Meaning |
|---|---|
`prefix${T}` | T appended to prefix |
`${T}suffix` | T prepended |
`${A}${B}` | Concatenation |
`${T}` | Same as T for strings |
| “` | Literal backtick |
\$ | Literal dollar |
Placeholder Types
| Type | Allowed |
|---|---|
string | ✅ |
number | ✅ |
bigint | ✅ |
boolean | ✅ |
null | ✅ |
undefined | ✅ |
| Literal types | ✅ |
| Unions | ✅ |
any | Widens to string |
never | Result is never |
| Objects | ❌ |
| Arrays | ❌ |
| Functions | ❌ |
Case Helpers
| Helper | Transforms |
|---|---|
Uppercase<T> | 'a' → 'A' |
Lowercase<T> | 'A' → 'a' |
Capitalize<T> | 'abc' → 'Abc' |
Uncapitalize<T> | 'Abc' → 'abc' |
Distribution Rules
| Placeholder | Result |
|---|---|
| Single literal | One string |
| Union | Union of combinations |
never | never |
any | string |
string | Template type (open) |
Common Patterns
| Pattern | Purpose |
|---|---|
`${P}${string}` | Prefix match |
`${string}${S}` | Suffix match |
`${infer X}${S}` | Extract prefix |
`${S}${infer X}` | Extract suffix |
`${infer A}-${infer B}` | Split on separator |
`get${Capitalize<K>}` | Getter name |
`set${Capitalize<K>}` | Setter name |
`on${Capitalize<K>}` | Handler name |
Mapped Type Patterns
| Pattern | Result |
|---|---|
`get${Capitalize<string & K>}` | Prefix keys |
`on${Capitalize<string & K>}` | Handler keys |
`[K in keyof T as \`prefix_${K}\`]` | Add prefix |
`[K in keyof T as K extends \`get${infer R}\` ? Uncapitalize<R> : K]` | Remove prefix |
`[K in keyof T as K extends \`get${string}\` ? K : never]` | Filter by prefix |
string & K Idiom
| Context | Why |
|---|---|
| Template in mapped type | K may be symbol or number |
string & K | Narrows to string |
Capitalize<string & K> | Ensures string input |
| Without it | Compile error |
Filtering with Extract
| Pattern | Result |
|---|---|
`Extract<T, \`prefix${string}\`>` | Members starting with prefix |
`Extract<T, \`${string}suffix\`>` | Members ending with suffix |
`Extract<T, \`${string}\`>` | All string members |
`Extract<T, \`${'a' | 'b'}${string}\`>` | Members with specific start |
Recursive Patterns
| Pattern | Purpose |
|---|---|
`Split<S, Sep>` | Split by separator |
`Join<T, Sep>` | Join tuple with separator |
`Replace<S, From, To>` | Replace substrings |
`Trim<S>` | Remove whitespace |
Standard Template Types
| Type | Meaning |
|---|---|
`${number}` | Numeric strings |
`${number}px` | CSS pixel values |
`v${number}.${number}.${number}` | Version strings |
`${boolean}` | 'true' or 'false' |
`${bigint}` | Bigint literals |
Common Utilities Built on Templates
| Type | Purpose |
|---|---|
Uppercase<T> | Uppercase strings |
Lowercase<T> | Lowercase strings |
Capitalize<T> | Capitalize first letter |
Uncapitalize<T> | Lowercase first letter |
Error Cases
| Error | Cause | Fix |
|---|---|---|
not assignable | String doesn’t match pattern | Check the pattern |
Type symbol not allowed | Placeholder is symbol | Use string & K |
| Recursive depth exceeded | Too deep | Simplify |
never result | Placeholder is never | Check input |
Best Practices
✅ Do This:
// Use templates for pattern-based strings
type Event = `on${'Click' | 'Hover'}`; // ✅
// Use case helpers for name generation
type Getter<K extends string> = `get${Capitalize<K>}`; // ✅
// Use `string & K` in mapped types
type G<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: T[K] }; // ✅
// Filter keys with Extract
type UserEvents = Extract<EventName, `user${string}`>; // ✅
// Use infer to extract parts
type Split<S> = S extends `${infer A}-${infer B}` ? [A, B] : never; // ✅
// Recurse for multi-split
type Parts<S> = S extends `${infer H}.${infer T}`
? [H, ...Parts<T>]
: [S]; // ✅
// Combine with mapped types for API generation
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
}; // ✅
// Use `never` to drop keys
type OnlyGetters<T> = {
[K in keyof T as K extends `get${string}` ? K : never]: T[K];
}; // ✅
// Test for valid strings with template types
type CSSValue = `${number}px`; // ✅
❌ Don’t Do This:
// Don't use templates where a plain string works
type Name = `${string}`; // same as string // ⚠️
// Don't forget string & K
type Bad<T> = { [K in keyof T as `get${Capitalize<K>}`]: T[K] };
// Error: K may not be a string // ❌
// Don't expect template placeholders to accept objects
type Bad = `${Date}`; // ❌ Date isn't allowed // ❌
// Don't mix template literal types with runtime templates
const x = `hello ${name}`; // runtime value, not a type // ⚠️
// Don't assume distribution produces a single string
type Bad = `prefix${'a' | 'b'}`;
// 'prefixa' | 'prefixb' — a union, not one string // ⚠️
// Don't go overboard with recursion
type Deep = /* very deep recursion */; // ⚠️
// Don't use template literals for validation
type Email = `${string}@${string}`;
// matches 'a@b' but also '@@' // ⚠️
// Don't forget that `${string}` accepts empty string
const x: `hello ${string}` = 'hello '; // ✅
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing string & K | Compile error | Cast K to string |
| Expecting single string | Got union | Understand distribution |
| Infinite recursion | Depth exceeded | Add base case |
| Empty string match | \prefix${string}`matches‘prefix’` | Add required separator |
Date placeholder | Not allowed | Convert to string first |
\${string}“ | Same as string | Not useful alone |
Filter with Extract | Wrong syntax | Match template exactly |
symbol in key | Compile error | string & K |
Case helper on string | Returns string | Only works on literals |
| Backtick escaping | Wrong chars | \``, $` |
Real-World Examples
1. Event name
type Event = `on${'Click' | 'Hover' | 'Focus'}`;
2. Getter name
type Getter<K extends string> = `get${Capitalize<K>}`;
3. CSS unit
type Px = `${number}px`;
4. Version string
type Version = `v${number}.${number}.${number}`;
5. Prefix match
type IsUserEvent<S> = S extends `user${string}` ? true : false;
6. Extract prefix
type Prefix<S> = S extends `${infer P}${string}` ? P : never;
7. Split on dash
type Split<S> = S extends `${infer A}-${infer B}` ? [A, B] : never;
8. Getter key from method
type Key<S> = S extends `get${infer K}` ? Uncapitalize<K> : never;
9. Mapped getter type
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
10. Mapped setter type
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
};
11. Handler type
type Handlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (p: T[K]) => void;
};
12. Remove prefix
type RemoveGet<T> = {
[K in keyof T as K extends `get${infer R}` ? Uncapitalize<R> : K]: T[K];
};
13. Filter by prefix
type OnlyOn<T> = {
[K in keyof T as K extends `on${string}` ? K : never]: T[K];
};
14. Add prefix to keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
15. Route parameter
type Param<S> = S extends `:${infer P}` ? P : never;
16. Extract params
type Params<S> =
S extends `${string}:${infer P}/${infer Rest}`
? P | Params<`/${Rest}`>
: S extends `${string}:${infer P}`
? P
: never;
type A = Params<'/users/:id/posts/:postId'>;
// 'id' | 'postId'
17. Split any separator
type Split<S extends string, Sep extends string> =
S extends `${infer H}${Sep}${infer T}`
? [H, ...Split<T, Sep>]
: [S];
type A = Split<'a.b.c', '.'>;
// ['a', 'b', 'c']
18. Uppercase keys
type UpperKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};
19. Replace suffix
type ReplaceSuffix<S, From extends string, To extends string> =
S extends `${infer P}${From}` ? `${P}${To}` : S;
type A = ReplaceSuffix<'user_id', '_id', ''>;
// 'user'
20. Convert to camelCase
type Camel<S extends string> =
S extends `${infer H}_${infer T}`
? `${H}${Capitalize<Camel<T>>}`
: S;
type A = Camel<'user_first_name'>;
// 'userFirstName'
Visual: Template Literal Type
┌──────────────────────────────────────────────┐
│ type Greeting = `hello ${string}`; │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Matches: │
│ │
│ 'hello world' ✅ │
│ 'hello ' ✅ │
│ 'hello 123' ✅ │
│ │
│ Doesn't match: │
│ │
│ 'hi world' ❌ │
│ 'hello' ❌ │
│ 'Hello world' ❌ │
│ │
└──────────────────────────────────────────────┘
Visual: Union Distribution
┌──────────────────────────────────────────────┐
│ type Size = 'sm' | 'md' | 'lg'; │
│ type Color = 'red' | 'blue'; │
│ │
│ type ClassName = `${Size}-${Color}`; │
│ │
└──────────────────────────────────────────────┘
│
│ Cartesian product
▼
┌──────────────────────────────────────────────┐
│ 'sm-red' │
│ 'sm-blue' │
│ 'md-red' │
│ 'md-blue' │
│ 'lg-red' │
│ 'lg-blue' │
│ │
│ 6 combinations │
│ │
└──────────────────────────────────────────────┘
Visual: Case Helpers
┌──────────────────────────────────────────────┐
│ 'hello' │
│ │ │
│ ├──► Uppercase → 'HELLO' │
│ ├──► Capitalize → 'Hello' │
│ ├──► Lowercase → 'hello' │
│ └──► Uncapitalize → 'hello' │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 'Hello' │
│ │ │
│ ├──► Uppercase → 'HELLO' │
│ ├──► Capitalize → 'Hello' │
│ ├──► Lowercase → 'hello' │
│ └──► Uncapitalize → 'hello' │
│ │
└──────────────────────────────────────────────┘
Visual: Pattern Matching with infer
┌──────────────────────────────────────────────┐
│ type Split<S> = │
│ S extends `${infer A}-${infer B}` │
│ ? [A, B] │
│ : never; │
│ │
│ Split<'user-123'> │
│ │
└──────────────────────────────────────────────┘
│
│ match
▼
┌──────────────────────────────────────────────┐
│ Match pattern: A-B │
│ 'user-123' │
│ ↑ ↑ │
│ │ └─── B = '123' │
│ └───── A = 'user' │
│ │
│ Result: ['user', '123'] │
│ │
└──────────────────────────────────────────────┘
Visual: Key Remapping
┌──────────────────────────────────────────────┐
│ interface User { │
│ name: string; │
│ age: number; │
│ } │
│ │
│ type Getters<T> = { │
│ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];│
│ }; │
│ │
└──────────────────────────────────────────────┘
│
│ for each key
▼
┌──────────────────────────────────────────────┐
│ 'name' → 'getName' │
│ 'age' → 'getAge' │
│ │
│ Result: { │
│ getName: () => string; │
│ getAge: () => number; │
│ } │
│ │
└──────────────────────────────────────────────┘
Visual: Filter with never
┌──────────────────────────────────────────────┐
│ interface Api { │
│ getName: () => string; │
│ getAge: () => number; │
│ version: string; │
│ } │
│ │
│ type OnlyGetters<T> = { │
│ [K in keyof T as K extends `get${string}` │
│ ? K : never]: T[K]; │
│ }; │
│ │
└──────────────────────────────────────────────┘
│
│ filter
▼
┌──────────────────────────────────────────────┐
│ 'getName' → matches → keep │
│ 'getAge' → matches → keep │
│ 'version' → doesn't → never → dropped │
│ │
│ Result: { │
│ getName: () => string; │
│ getAge: () => number; │
│ } │
│ │
└──────────────────────────────────────────────┘
Visual: Recursive Split
┌──────────────────────────────────────────────┐
│ type Split<S, Sep> = │
│ S extends `${infer H}${Sep}${infer T}` │
│ ? [H, ...Split<T, Sep>] │
│ : [S]; │
│ │
│ Split<'a.b.c', '.'> │
│ │
└──────────────────────────────────────────────┘
│
│ step 1
▼
┌──────────────────────────────────────────────┐
│ H = 'a', T = 'b.c' │
│ → ['a', ...Split<'b.c', '.'>] │
│ │
└──────────────────────────────────────────────┘
│
│ step 2
▼
┌──────────────────────────────────────────────┐
│ H = 'b', T = 'c' │
│ → ['a', 'b', ...Split<'c', '.'>] │
│ │
└──────────────────────────────────────────────┘
│
│ step 3
▼
┌──────────────────────────────────────────────┐
│ 'c' has no more '.' → ['c'] │
│ │
│ Result: ['a', 'b', 'c'] │
│ │
└──────────────────────────────────────────────┘
Visual: Common Template Types
┌──────────────────────────────────────────────┐
│ `${number}` │
│ ── matches any numeric string │
│ │
│ `${number}px` │
│ ── CSS pixel values │
│ │
│ `get${Capitalize<string>}` │
│ ── getter names │
│ │
│ `on${Capitalize<string>}` │
│ ── event handler names │
│ │
│ `v${number}.${number}.${number}` │
│ ── semantic versions │
│ │
│ `${boolean}` │
│ ── 'true' | 'false' │
│ │
└──────────────────────────────────────────────┘
Visual: string & K Idiom
┌──────────────────────────────────────────────┐
│ type Getters<T> = { │
│ [K in keyof T as `get${Capitalize<K>}`]: () => T[K];│
│ }; │
│ │
│ Error: K may not be a string │
│ (K could be number or symbol) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ type Getters<T> = { │
│ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];│
│ }; │
│ │
│ ✅ Compiles │
│ string & K is the string part of K │
│ │
└──────────────────────────────────────────────┘
Visual: When to Use Which
┌──────────────────────────────────────────────┐
│ Pattern-based string type? │
│ │ │
│ └── Template literal type │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Extract from a string type? │
│ │ │
│ └── Template + infer │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Transform keys? │
│ │ │
│ └── Mapped type + `as` template │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Generate all combinations? │
│ │ │
│ └── Template with union placeholders │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Split a string type? │
│ │ │
│ └── Recursive infer with separator │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Template literal type | String type with ${} placeholders |
| Placeholder | string, number, literals, unions |
| Distribution | Union of all combinations |
| Case helpers | Uppercase, Lowercase, Capitalize, Uncapitalize |
infer | Extract from a template pattern |
| Key remapping | Mapped type + as template |
string & K | Narrows key to string |
never filter | Drop non-matching keys |
| Recursion | Multi-split, transform |
Key takeaways:
- A template literal type uses backticks and
${}to describe strings by pattern - Placeholders accept
string,number,bigint,boolean,null,undefined, literals, and unions - Union placeholders distribute —
\${‘a’ | ‘b’}-${‘x’ | ‘y’}“ produces four strings - The case helpers —
Uppercase,Lowercase,Capitalize,Uncapitalize— transform string types inferinside a template extracts parts —S extends `${infer A}-${infer B}`splits on-- Mapped types +
asrename keys —`get${Capitalize<string & K>}`generates getters string & KnarrowsKto a string in template contexts- Mapping a key to
neverdrops it — the standard filter idiom - Recursion handles multi-split and transform —
Split<'a.b.c', '.'>→['a', 'b', 'c'] Extract<T, \prefix${string}`>` filters a union of strings by prefix- Reach for template literal types when the string follows a pattern and you want the type system to know it
Remember: Template literal types are pattern-based string types. They compose with unions (Cartesian products), infer (pattern matching), case helpers (name generation), and mapped types (key remapping). They’re how libraries type routes, events, CSS properties, and generated methods. Learn the syntax, the distribution rules, and the string & K idiom, and you can describe almost any structured string at the type level — plus extract and transform the pieces.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!