Template Literal Types
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).
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.
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.
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).
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.
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.
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' // errorUseful for design-token APIs where you want the variable name spelled correctly.
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
// "userFullName"
type R = SnakeToCamel<'user_full_name'>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]
}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 prefixThis single-handedly catches “I passed an OrderId where a UserId was expected” bugs.
Gotchas
- 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.
Related
Further reading
Interview angle 5
- “What are template literal types actually used for?” - building fully
typed APIs out of string patterns. It is how a typed router extracts
":id"from a path into a params object, how tRPC and typed event emitters derive names, and how i18n libraries check a translation key exists - all at compile time from a string. - “How do you parse a string in the type system?” -
inferinside the template:T extends `${infer Head}-${infer Rest}`splits it, and you recurse onRest. That plusCapitalize/Uncapitalizeis the whole snake_case-to-camelCase trick. - “What is the limit?” - combinatorial explosion.
${A}-${B}-${C}over three 50-member unions is 125,000 string literal types and the compiler gives up. That is why nobody types “every Tailwind class” this way - those libraries generate the types instead.- Follow-up: “Why does it explode so fast?” - template literals distribute
over unions, so
`pre-${A | B}`automatically becomes`pre-A` | `pre-B`. That is exactly what you want for two small unions and what kills you for three large ones.
- Follow-up: “Why does it explode so fast?” - template literals distribute
over unions, so
- “You map over
keyof Tinto a template and it errors. Why?” -keyof Tincludesnumber | symbol, and a template literal needs a string. Intersect first:K & string. It is the single most common error in this area. - “
asappears in mapped types and in expressions. Same thing?” - no, and the shared keyword is unfortunate. In a mapped type,asremaps the key and is a real type-level operation. In an expression it is a cast that is erased at runtime and checks nothing.