| |

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 typeExample
stringAny string
numberAny number
bigintAny bigint
booleantrue or false
nullnull
undefinedundefined
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 infer captures 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 & K is needed: K can be a string, number, or symbol. Template literals only accept the first three (and a few more). string & K says “the string part of K” — narrowing K to 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 with as to rename every key to on${Capitalize<K>}
  • AppHandlers — generated handler methods from the event map
  • Extract<EventName, \user${string}`>` — filter event names by prefix
  • ActionOf<K> — split an event name into the entity and action parts
  • Emitter<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

FormMeaning
`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

TypeAllowed
string
number
bigint
boolean
null
undefined
Literal types
Unions
anyWidens to string
neverResult is never
Objects
Arrays
Functions

Case Helpers

HelperTransforms
Uppercase<T>'a''A'
Lowercase<T>'A''a'
Capitalize<T>'abc''Abc'
Uncapitalize<T>'Abc''abc'

Distribution Rules

PlaceholderResult
Single literalOne string
UnionUnion of combinations
nevernever
anystring
stringTemplate type (open)

Common Patterns

PatternPurpose
`${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

PatternResult
`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

ContextWhy
Template in mapped typeK may be symbol or number
string & KNarrows to string
Capitalize<string & K>Ensures string input
Without itCompile error

Filtering with Extract

PatternResult
`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

PatternPurpose
`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

TypeMeaning
`${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

TypePurpose
Uppercase<T>Uppercase strings
Lowercase<T>Lowercase strings
Capitalize<T>Capitalize first letter
Uncapitalize<T>Lowercase first letter

Error Cases

ErrorCauseFix
not assignableString doesn’t match patternCheck the pattern
Type symbol not allowedPlaceholder is symbolUse string & K
Recursive depth exceededToo deepSimplify
never resultPlaceholder is neverCheck 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

PitfallProblemSolution
Missing string & KCompile errorCast K to string
Expecting single stringGot unionUnderstand distribution
Infinite recursionDepth exceededAdd base case
Empty string match\prefix${string}`matches‘prefix’`Add required separator
Date placeholderNot allowedConvert to string first
\${string}“Same as stringNot useful alone
Filter with ExtractWrong syntaxMatch template exactly
symbol in keyCompile errorstring & K
Case helper on stringReturns stringOnly works on literals
Backtick escapingWrong 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

ConceptMeaning
Template literal typeString type with ${} placeholders
Placeholderstring, number, literals, unions
DistributionUnion of all combinations
Case helpersUppercase, Lowercase, Capitalize, Uncapitalize
inferExtract from a template pattern
Key remappingMapped type + as template
string & KNarrows key to string
never filterDrop non-matching keys
RecursionMulti-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 helpersUppercase, Lowercase, Capitalize, Uncapitalize — transform string types
  • infer inside a template extracts parts — S extends `${infer A}-${infer B}` splits on -
  • Mapped types + as rename keys — `get${Capitalize<string & K>}` generates getters
  • string & K narrows K to a string in template contexts
  • Mapping a key to never drops 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!