Skip to content
webtype.orgwebtype.org

    ↑↓ move · ⏎ open · esc close

    No. 253 · September 5, 2026 · Hard

    Snake case

    Implement SnakeCase<S> so "helloWorld" becomes "hello_world". Only letters count as capitals — digits and symbols must pass through untouched.

    01

    Try the puzzle yourself

    Par 4

    Puzzle

    snake-case.ts
    Stroke 1 of 4Not run yet

    Replace ??? — your solution is checked against the cases below. Tab indents; press Escape then Tab to move focus out.

    Checks

    4
    • SnakeCase<'helloWorld'>—
      → 'hello_world'
    • SnakeCase<'a1B'>—
      → 'a1_b'
    • SnakeCase<'foo'>—
      → 'foo'
    • SnakeCase<''>—
      → ''

    How a check is judged Exact type equality, not assignability — an intersection is not the same as the flattened object.

    How everyone did

    Fewer than 5 people have solved this one so far. The distribution appears once there is enough of a sample to mean anything.

    Short game

    Fewest characters

    No public scores yet.

    Archive
    02

    Annotated solution

    Published September 6, 2026

    The solution

    type SnakeCase<S extends string> =
      S extends `${infer H}${infer R}`
        ? H extends Uppercase<H>
          ? H extends Lowercase<H>
            ? `${H}${SnakeCase<R>}`
            : `_${Lowercase<H>}${SnakeCase<R>}`
          : `${H}${SnakeCase<R>}`
        : S

    The common wrong answer

    type SnakeCase<S extends string> =
      S extends `${infer H}${infer R}`
        ? H extends Uppercase<H>
          ? `_${Lowercase<H>}${SnakeCase<R>}`
          : `${H}${SnakeCase<R>}`
        : S

    "1" extends Uppercase<"1"> is true, because Uppercase leaves anything that is not a lowercase letter exactly as it found it. So every digit, space and symbol is treated as a capital and gets its own underscore: "a1B" comes out as "a_1_b". Case-ness is not one test but two — equal to your uppercase form, *and* different from your lowercase one.

    Line by line

    1. `${infer H}${infer R}`

      Two placeholders with nothing between them split off exactly one character. This is the type-level equivalent of walking a string one index at a time.

    2. H extends Lowercase<H> ? ... : ...

      The second test is what excludes non-letters. A digit is equal to both its uppercase and its lowercase form, so it lands in the branch that passes it through unchanged.

    3. : S

      The empty string matches no one-character pattern, so it falls to the base case and returns itself — which is also what stops the recursion.

    Takeaway

    Uppercase and Lowercase are total functions: they never fail, they just do nothing to characters that have no case. Any test built on one of them alone will quietly misclassify every digit and symbol you meet.

    Uses