#19458β€’Hard

SnakeCase

SnakeCase<T> converts camelCase string types to snake_case by scanning one character at a time. Union inputs work for free thanks to distributive conditionals.

Walk a camelCase string one character at a time and swap every uppercase letter for an underscore plus its lowercase form.

The conversion here goes from camelCase to snake_case. Uppercase letters are the only word markers you get, so you scan the string character by character and insert an underscore wherever the case changes. Along the way you meet distributive conditional types, since the type has to convert every member of a union independently.

A few examples:

type res1 = SnakeCase<"hello">; // => "hello"
type res2 = SnakeCase<"userName">; // => "user_name"
type res3 = SnakeCase<"getElementById">; // => "get_element_by_id"

Challenge Instructions: SnakeCase

Hard

Create a SnakeCase<T> generic that turns a string formatted in camelCase into a string formatted in snake_case.

A few examples:

type res1 = SnakeCase<"hello">; // => "hello"
type res2 = SnakeCase<"userName">; // => "user_name"
type res3 = SnakeCase<"getElementById">; // => "get_element_by_id"

View on GitHub: https://tsch.js.org/19458

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 is a single recursive type:

type SnakeCase<T> = T extends `${infer Ch}${infer Rest}`
  ? Ch extends Lowercase<Ch>
    ? `${Ch}${SnakeCase<Rest>}`
    : `_${Lowercase<Ch>}${SnakeCase<Rest>}`
  : T

Compared to CamelCase, this direction is the easy one: in camelCase input every uppercase letter marks the start of a new word, so the whole algorithm is "walk the string; whenever you see an uppercase letter, replace it with an underscore plus its lowercase form".

Peeling off one character at a time

T extends `${infer Ch}${infer Rest}` uses a key template literal inference rule: when two infer placeholders are adjacent, the first one matches exactly one character and the second takes everything else. For 'userName' the first step gives:

// Ch   = 'u'
// Rest = 'serName'

Each recursive call consumes one character, so the type walks the entire string front to back.

Classifying the character

Ch extends Lowercase<Ch> asks: is this character already lowercase? For 'u', Lowercase<'u'> is 'u', the check passes, and we keep the character unchanged: `${Ch}${SnakeCase<Rest>}`.

For 'N', Lowercase<'N'> is 'n', and 'N' extends 'n' is false: we've found a word boundary. The second branch emits an underscore plus the lowered letter: `_${Lowercase<Ch>}${SnakeCase<Rest>}`.

Tracing 'userName':

// 'u', 's', 'e', 'r' β†’ kept as-is
// 'N' β†’ '_n'
// 'a', 'm', 'e' β†’ kept as-is
// result: 'user_name'

Note that characters without a case (digits, _, symbols) satisfy Ch extends Lowercase<Ch> too, so they'd pass through untouched rather than sprouting spurious underscores.

The base case

When T no longer matches the one-character pattern, the string is exhausted ('') and we return T itself. At that point T is '', so this is the identity on the empty string.

Union distribution for free

Look at the last test case:

SnakeCase<'getElementById' | 'getElementByClassNames'>
// => 'get_element_by_id' | 'get_element_by_class_names'

The type must convert each union member separately, not the union as a whole. That's what a distributive conditional type does: when the checked type in T extends ... is a bare (naked) type parameter, TypeScript automatically applies the conditional to every member of a union and unions the results back together. Because our very first line is T extends a template literal pattern with T naked, distribution happens for free, and each string in the union runs through the recursion independently. (This has nothing to do with T lacking an extends string constraint on the signature, by the way; a constrained SnakeCase<T extends string> would distribute the same way. What matters is that the conditional checks T directly, not something derived from it.)

Edge cases the tests cover

If you solved CamelCase before this, notice the symmetry: both walk strings via single-character inference, but here detection is a plain lowercase check because the input format guarantees uppercase letters only appear at word boundaries.

This challenge is originally from here.

Share this challenge

Learn the Concepts