frontend / typescript / 03_template_literal_types.md

Template Literal Types

4 min read source

Template Literal Types

TL;DR

Template literal types are template strings, but at the type level — `prefix_${T}` builds new string-literal types from existing ones. Combined with key remapping in mapped types, they’re how libraries build type-safe APIs around naming conventions (onClickonclick, route params → param object types, event names → handler signatures).

Interview Q&A

Q: What’s a template literal type?

A type that’s a string built from other string-literal types. The ${...} placeholder accepts unions, and the type distributes across them.

type Color = 'red' | 'blue'
type Size = 'sm' | 'lg'

type Variant = `${Color}-${Size}`
// = "red-sm" | "red-lg" | "blue-sm" | "blue-lg"

A literal cross-product, computed at type-check time.

Q: What are the intrinsic string manipulation types?

Four built-ins operate on string literal types:

Type Effect
Uppercase<S> 'abc''ABC'
Lowercase<S> 'ABC''abc'
Capitalize<S> 'name''Name'
Uncapitalize<S> 'Name''name'
type Loud<S extends string> = `${Uppercase<S>}!`
type R = Loud<'hello'>  // "HELLO!"

These are the building blocks for property-name transformations.

Q: How do you combine template literal types with mapped types?

Use key remapping with as to transform property names. The classic example is generating a getter for every property:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}

interface User { id: number; name: string }
type G = Getters<User>
// = { getId: () => number; getName: () => string }

K & string filters to string keys (since keyof may include number | symbol).

Q: How would you type a route param extractor?

type ExtractParams<Path extends string> =
  Path extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<Rest>]: string } :
  Path extends `${string}:${infer Param}` ? { [K in Param]: string } :
  {}

type P1 = ExtractParams<'/users/:id'>
// = { id: string }

type P2 = ExtractParams<'/orgs/:org/users/:user'>
// = { org: string; user: string }

This is how libraries like TanStack Router achieve fully typed params from a path string.

Q: How would you build type-safe event names?

type EventMap = {
  click: { x: number; y: number }
  hover: { target: string }
  scroll: { offset: number }
}

type Handlers = {
  [K in keyof EventMap as `on${Capitalize<K & string>}`]: (payload: EventMap[K]) => void
}
// = { onClick: (p: { x; y }) => void; onHover: ...; onScroll: ... }

This is the pattern under Mantine/MUI prop types and Vue emit typing.

Q: How do you type a CSS variable name pattern?

type CSSVar<Name extends string> = `--${Name}`

const v: CSSVar<'primary-color'> = '--primary-color'
const w: CSSVar<'primary-color'> = '--bad'  // error

Useful for design-token APIs where you want the variable name spelled correctly.

Q: How would you build a “snake_case to camelCase” type?

type SnakeToCamel<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
    : S

type R = SnakeToCamel<'user_full_name'>  // "userFullName"

Recurses on the underscore. Often combined with a mapped type to transform entire object shapes.

type CamelizeKeys<T> = {
  [K in keyof T as SnakeToCamel<K & string>]: T[K]
}

Q: Can you use template literal types to constrain a string?

Yes — useful for IDs and prefixed enums:

type UserId = `usr_${string}`
type OrderId = `ord_${string}`

function findUser(id: UserId) { /* ... */ }

findUser('usr_abc')   // ok
findUser('ord_xyz')   // error — wrong prefix

This single-handedly catches “I passed an OrderId where a UserId was expected” bugs.

Gotchas / edge cases

  • Combinatorial explosion${A}-${B}-${C} with unions of 50, 50, 50 = 125,000 string types. TS will balk.
  • Distributes over unions`pre-${A | B}` becomes `pre-A` | `pre-B` automatically.
  • number in template literal types`${number}` matches any number-shaped string, useful for parsing.
  • infer inside a template literal — yes, that’s how the parsing tricks work (extends `${infer A}-${infer B}`).
  • K & string — when iterating keyof T, intersect with string to drop number | symbol keys before passing to Capitalize.
  • Bivariance and asas in a mapped type remaps the key, while as in an expression is a runtime-erased cast. Different things, same keyword.

What a senior is expected to say

A junior treats template literal types as a curiosity. A senior knows they’re how modern libraries (TanStack Router/Query, tRPC, validation libraries, type-safe i18n) build their fully-typed APIs from string patterns. The senior also flags the limit: TS gives up on excessively large unions, so building “all CSS class combinations” or “all Tailwind classes” via template literals doesn’t scale — those libraries use generation or other tricks instead.

Cross-references

Further reading