Concept 9 of 12
Template literal types
Template literal types let the compiler build and match strings. Combined with `infer` and recursion they can parse — which is how a route string becomes a params object, and how typed i18n keys and ORM column names are checked at compile time.
Building and stringifying
type Greeting = `hello ${string}` type Event = `on${'Click' | 'Focus'}` // 'onClick' | 'onFocus' type AsText = `${42}` // '42' // Interpolating a union multiplies out every combination. type Cell = `${'a' | 'b'}${1 | 2}` // 'a1' | 'a2' | 'b1' | 'b2'
Interpolating a union produces the cross product, which grows fast — TypeScript caps the result at 100,000 members and errors past that. Stringifying a number with `` `${N}` `` is the standard way to turn an arithmetic problem into a text problem.
Matching is anchored and greedy from the left
type StartsWith<S, P extends string> = S extends `${P}${string}` ? true : false type EndsWith<S, P extends string> = S extends `${string}${P}` ? true : false type Contains<S, P extends string> = S extends `${string}${P}${string}` ? true : false // Greedy from the left: Head binds the SHORTEST prefix that works. type Split1<S> = S extends `${infer Head}-${infer Tail}` ? [Head, Tail] : never type X = Split1<'a-b-c'> // ['a', 'b-c']
A pattern must match the whole string, so where you put `${string}` is where you allow slack. Slack at the front means "contains", at the back means "starts with", and on both sides means you have accidentally written a substring test.
The four intrinsic case types
Uppercase<'abc'> // 'ABC' Lowercase<'ABC'> // 'abc' Capitalize<'abc'> // 'Abc' Uncapitalize<'Abc'> // 'abc'
These four are implemented natively in the compiler and have no TypeScript source. Reach for them before writing a character-by-character recursion — `CamelCase` and friends are a few lines with them and a nightmare without.
The common wrong answer
type Replace<S extends string, From extends string, To extends string> = S extends `${infer H}${From}${infer T}` ? `${H}${To}${T}` : S Replace<'abc', '', 'x'> // 'xabc' — not 'abc'
The empty string sits between any two characters, so an empty needle matches immediately with `H` empty. It is the `null` of template literal types: it matches everywhere and quietly breaks patterns that assume a match means something was found.
Takeaway
Patterns are anchored at both ends and greedy from the left. Where you allow slack decides what you are actually asking, and the empty string is always the edge case.