Format<T> turns a printf-style format string into a curried function type, one typed argument per placeholder. The twist: the recursion builds function types, not tuples.
Format<T extends string> reads a printf-style format string and produces a curried function type: each %s placeholder demands a string argument, each %d demands a number, and once all placeholders are consumed the result is string. It's the type-level idea behind fully type-safe formatting and translation APIs, where passing the wrong number or kind of arguments is a compile error rather than a runtime surprise.
For example (the comments use string => string as shorthand for the curried function type (s1: string) => string):
type FormatCase1 = Format<"%sabc"> // FormatCase1 : string => string
type FormatCase2 = Format<"%s%dabc"> // FormatCase2 : string => number => string
type FormatCase3 = Format<"sdabc"> // FormatCase3 : string
type FormatCase4 = Format<"sd%abc"> // FormatCase4 : stringImplement Format<T extends string> generic.
For example,
type FormatCase1 = Format<"%sabc"> // FormatCase1 : string => string
type FormatCase2 = Format<"%s%dabc"> // FormatCase2 : string => number => string
type FormatCase3 = Format<"sdabc"> // FormatCase3 : string
type FormatCase4 = Format<"sd%abc"> // FormatCase4 : stringView on GitHub: https://tsch.js.org/545
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 solution:
type Format<T extends string> =
T extends `${infer _Prefix}%${infer Ch}${infer Rest}`
? Ch extends 's'
? (s1: string) => Format<Rest>
: Ch extends 'd'
? (d1: number) => Format<Rest>
: Format<Rest>
: stringCompared to other string parsers, the twist is that each recursion step wraps the remainder in a function type instead of accumulating results into a tuple.
The pattern `${infer _Prefix}%${infer Ch}${infer Rest}` relies on two template literal inference rules:
_Prefix matches the shortest possible prefix, so the pattern always finds the first % in the string.infer placeholders split so that the first (Ch) matches exactly one character and the second (Rest) takes everything after it.For T = 'a%dbc%s' the first match evaluates to:
// _Prefix = 'a'
// Ch = 'd'
// Rest = 'bc%s'_Prefix is inferred but never used. The literal text around placeholders doesn't influence the signature; only the placeholders do.
Here's the key idea. When Ch is 's', the result type is:
[object Object]That's a function that takes one string and returns whatever the rest of the format string demands. The recursion nests naturally: Format<'a%dbc%s'> becomes
(d1: number) => Format<'bc%s'>
// = (d1: number) => (s1: string) => Format<''>
// = (d1: number) => (s1: string) => stringEach placeholder adds one layer of currying, and the innermost return type is always string, the fully formatted result. This is a pattern worth remembering: recursive conditional types can build up function types, not just strings and tuples.
If the character after % is neither 's' nor 'd', we fall through to the plain Format<Rest>: no parameter is added, and parsing continues after that character. That's what makes Format<'sd%abc'> evaluate to string. The only % is followed by 'a', which isn't a valid control character, so nothing is emitted and 'bc' contains no further placeholders.
When no % followed by at least one character remains, the pattern fails and we return string. This handles Format<'sdabc'> (no % at all) and terminates every recursion, since eventually Rest runs out of placeholders.
%% escape, for freeTwo test cases probe escaped percent signs:
Format<'a%%dbc'> should be string. The pattern finds the first %, so Ch is the second %, not 's' or 'd', and we recurse on 'dbc', which has no % left. The pair %% escaped itself, just like in C.Format<'a%%%dbc'> should be (d1: number) => string. The first two percent signs pair up and are skipped, but Rest is '%dbc', so the third % still produces a number parameter.Notice there's no explicit escaping logic: consuming the character after % as Ch means the second % of a pair can never start a placeholder of its own. If you enjoyed this, the sibling challenge C-printf Parser (#147) extracts the same placeholders into a tuple instead of a signature; the two make a good pair.
This challenge is originally from here.