#545Hard

printf

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 :  string

Challenge Instructions: printf

Hard

Implement 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 :  string

View 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.

Loading...

Detailed Explanation

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>
    : string

Compared 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.

Locating the next placeholder

The pattern `${infer _Prefix}%${infer Ch}${infer Rest}` relies on two template literal inference rules:

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.

Emitting one curried parameter per placeholder

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) => string

Each 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.

Unknown placeholders are skipped

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.

The base case

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.

The %% escape, for free

Two test cases probe escaped percent signs:

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.

Share this challenge

Learn the Concepts