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 (onClick → onclick, 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. numberin template literal types —`${number}`matches any number-shaped string, useful for parsing.inferinside a template literal — yes, that’s how the parsing tricks work (extends`${infer A}-${infer B}`).K & string— when iteratingkeyof T, intersect withstringto dropnumber | symbolkeys before passing toCapitalize.- Bivariance and
as—asin a mapped type remaps the key, whileasin 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
- Mapped types and
infer: 02_conditional_and_mapped_types.md - React event handler typing: 08_react_typing.md