Parse a C-style printf format string at the type level, collecting placeholders like %d into a tuple. Lazy template inference handles %% escapes for free.
One recursive template literal pattern is enough to parse printf format strings, %% escapes included.
There is a function in C language: printf. This function allows us to print something with formatting. Like this:
[object Object]This challenge asks you to parse the input string and extract the format placeholders like %d and %f. If the input is "The result is %d.", the parsed result is the tuple ['dec']. The toolkit, lazy template inference plus an accumulating recursion, is the same one libraries use to type-check SQL queries or route parameters from plain strings.
Here is the mapping:
type ControlsMap = {
c: 'char',
s: 'string',
d: 'dec',
o: 'oct',
h: 'hex',
f: 'float',
p: 'pointer',
}There is a function in C language: printf. This function allows us to print something with formatting. Like this:
[object Object]This challenge requires you to parse the input string and extract the format placeholders like %d and %f. For example, if the input string is "The result is %d.", the parsed result is a tuple ['dec'].
Here is the mapping:
type ControlsMap = {
c: 'char',
s: 'string',
d: 'dec',
o: 'oct',
h: 'hex',
f: 'float',
p: 'pointer',
}View on GitHub: https://tsch.js.org/147
Change the following code to make the test cases pass (no type check errors).
👋 Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The entire parser is a single recursive conditional type:
type ParsePrintFormat<S extends string> =
S extends `${infer _Prefix}%${infer Ch}${infer Rest}`
? Ch extends keyof ControlsMap
? [ControlsMap[Ch], ...ParsePrintFormat<Rest>]
: ParsePrintFormat<Rest>
: []Compact, but every clause has a job.
%The pattern `${infer _Prefix}%${infer Ch}${infer Rest}` scans the string for a percent sign. Two inference rules make it behave the way a hand-written parser would:
_Prefix matches the shortest possible prefix, so the pattern always locks onto the first % in the string.infer placeholders sit directly next to each other, the first one (Ch) matches exactly one character, and Rest swallows everything after it.For S = 'Hello %s: score is %d.' the first match evaluates to:
// _Prefix = 'Hello '
// Ch = 's' (the single character right after the %)
// Rest = ': score is %d.'We never use _Prefix; the text before a placeholder is irrelevant. But we still have to infer it so the pattern can skip over it. The underscore prefix is just a naming convention for "intentionally unused".
%Ch extends keyof ControlsMap asks: is this one of the seven control characters? keyof ControlsMap evaluates to the union 'c' | 's' | 'd' | 'o' | 'h' | 'f' | 'p', so this is a plain membership test.
ControlsMap[Ch] is an indexed access that looks up the human-readable name: ControlsMap['d'] is 'dec', ControlsMap['f'] is 'float'. We prepend it to the result with a spread: [ControlsMap[Ch], ...ParsePrintFormat<Rest>]. This is the standard trick for building tuples recursively: each level contributes one element and splices in whatever the rest of the string produces.%q, or a second %), we contribute nothing and recurse on Rest.When the string contains no further % followed by at least one character, the pattern fails to match and we return []. That covers '', 'Any string.', and, more subtly, a string that ends with %: in 'The result is %' there is no character after the % for Ch to bind to, so the whole pattern fails and the result is [], which is what the tests expect.
The test suite includes two tricky cases built around %%:
'The result is %%d.' should produce []. The pattern finds the first %, so Ch is the second %, which is not a key of ControlsMap, and we recurse on 'd.', which contains no % at all. The %% effectively escaped itself, just like in real C.'The result is %%%d.' should produce ['dec']. The first two percent signs pair up and get skipped as above, but Rest is '%d.', so the recursion finds the third % followed by d and emits 'dec'.Notice that we never wrote special-case logic for escaping: treating an unknown character after % as "skip and continue" handles it for free, because the second % of a %% pair is consumed as Ch and can no longer start a placeholder.
The shape of this solution is the backbone of virtually every type-level parser: match lazily up to a delimiter, inspect one character, look it up in a map, recurse on the remainder while accumulating a tuple. Once it clicks here, you can apply it to URL route params (/users/:id), SQL placeholders, or the follow-up challenge printf (#545), which turns these placeholders into a typed function signature.
This challenge is originally from here.