Lesson 22 · Module 3.5 — Type-level programming
infer and recursionTemplate-literal types let a type look like a string with holes:
`${A}-${B}`. Drop infer into those holes and the type system becomes a tiny
pattern-matcher — it can split a string and capture the pieces. Add recursion and you have a parser that
pulls every {param} out of a path, entirely at compile time.
A template-literal pattern with infer — like
`${string}{${infer Name}}${infer Rest}` — matches a string and binds each
infer hole to the text that filled it. Name gets the part inside the
braces; Rest gets everything after, ready to feed back into the same type.
Start with a single step. The pattern below says “some prefix, then {…}, then a tail.”
The infer holes name the captured pieces:
type FirstParam<S extends string> =
S extends `${string}{${infer Name}}${infer Rest}` ? Name : never;
type A = FirstParam<"/users/{id}/posts">;
// Name = "id", Rest = "/posts" → resolves to "id"
Read the pattern left to right: ${string} soaks up "/users/", the literal
{ and } bracket the placeholder, infer Name grabs
"id", and infer Rest grabs "/posts". The conditional then returns
Name. We threw Rest away here — but recursion will reuse it.
To gather all params, return Name and call the type again on Rest.
A type calling itself is recursion; a base case stops it when no braces remain:
type GetParams<S extends string> =
S extends `${string}{${infer Name}}${infer Rest}`
? [Name, ...GetParams<Rest>] // found one → keep it, recurse on the tail
: []; // no more braces → stop, empty tuple
type P = GetParams<"/users/{id}/posts/{postId}">;
// resolves to: ["id", "postId"]
The ...GetParams<Rest> is a spread inside a tuple type: it splices the recursive
result onto the front element. Each call peels one placeholder and hands the rest back to itself.
Reading recursion means unrolling it. Substitute the string at each step until the base case fires:
GetParams<"/users/{id}/posts/{postId}">
// match: Name="id", Rest="/posts/{postId}"
=> ["id", ...GetParams<"/posts/{postId}">]
// match: Name="postId", Rest=""
=> ["id", "postId", ...GetParams<"">]
// "" has no braces → base case → []
=> ["id", "postId"]
Each line drops one placeholder and shortens the string until nothing matches and []
closes the tuple. That is the whole technique: a template-literal pattern to split, infer to
capture, and recursion on the tail to keep going.
Without the : [] fallback the recursion would have no way to stop, and TypeScript would
refuse it with a “type instantiation is excessively deep” error. Every type-level recursion needs a
branch where the pattern fails and returns a concrete result.
Recall, don’t re-read. (Answers reveal on click.)
S extends `${string}{${infer Name}}${infer Rest}`, what does Rest capture?: [] branch in the recursive GetParams?GetParams<"/a/{x}/b/{y}"> resolve to?The official handbook page is
TypeScript Handbook — Template Literal Types;
the inference-within-template section pairs it with
Inferring Within Conditional Types.
Best way to feel it: paste GetParams into the
TS Playground and hover the result as you add more
{…} segments to the input string.
Stuck unrolling a recursive type? Take it with a sample input and trace it one step at a time, substituting the string at each call until the base case fires. Next lesson (3.6) closes the module with a single dense idiom — turning a record of payloads into a discriminated union.