#114β€’Hard

CamelCase

Convert a snake_case string type to camelCase with recursive template literal inference. The tricky part: characters that cannot be uppercased, like _ and $.

Five lines of template literal inference turn any snake_case string into camelCase.

String manipulation done entirely in the type system: CamelCase<T> converts a snake_case string to camelCase. You split strings with template literal patterns and recurse through the pieces. The intrinsic Uppercase/Lowercase utilities detect whether a character is even a letter, which is where the tricky cases live. The same techniques power libraries that map API responses with snake_case keys onto idiomatic TypeScript objects.

For example

type camelCase1 = CamelCase<'hello_world_with_types'> // expected to be 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'> // expected to be same as previous one

Challenge Instructions: CamelCase

Hard

Implement CamelCase<T> which converts snake_case string to camelCase.

For example

type camelCase1 = CamelCase<'hello_world_with_types'> // expected to be 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'> // expected to be same as previous one

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

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 in full:

type CamelCase<S extends string> =
  S extends `${infer Left}_${infer Ch}${infer Rest}`
    ? Uppercase<Ch> extends Lowercase<Ch>
      ? `${Lowercase<Left>}_${CamelCase<`${Ch}${Rest}`>}`
      : `${Lowercase<Left>}${Uppercase<Ch>}${CamelCase<Rest>}`
    : Lowercase<S>

Five lines that take some unpacking.

Splitting on the first underscore

The pattern `${infer Left}_${infer Ch}${infer Rest}` is the engine of the solution. Template literal inference in TypeScript is lazy: Left matches the shortest possible prefix, so it captures everything up to the first underscore. There's a second trick hiding here too: when two infer placeholders sit next to each other, the first one (Ch) matches exactly one character, and Rest swallows whatever remains.

For S = 'foo_bar_baz' the pattern evaluates to:

// Left = 'foo'
// Ch   = 'b'   (the single character right after the underscore)
// Rest = 'ar_baz'

Isolating the single character after the underscore is the key move, because what we do next depends entirely on what that character is.

Detecting "is this a letter?"

Uppercase<Ch> extends Lowercase<Ch> looks strange at first, but it answers a useful question: does this character have a case at all?

So the condition is true exactly when Ch is not a cased letter.

The two branches

If Ch can't be capitalized (another underscore or a $, say), the underscore we split on must be preserved. There is no letter to merge it into. We emit `${Lowercase<Left>}_` and recurse on `${Ch}${Rest}`, gluing the odd character back onto the remainder. This is how 'foo__bar' becomes 'foo_Bar': the first underscore is the one we split on and re-emit (it survives as the literal _ in the output), while the second underscore is Ch. It gets glued back onto the remainder as '_bar', and in the next recursive step it becomes the separator that merges into 'B'.

If Ch is a letter, the underscore disappears and the letter gets promoted: `${Lowercase<Left>}${Uppercase<Ch>}`, then we recurse on Rest. For 'foo_bar' that produces 'foo' + 'B' + CamelCase<'ar'> = 'fooBar'.

The base case

When no underscore pattern matches anymore, we return Lowercase<S>. This handles both the tail of the recursion and the tests that never recurse at all: 'foobar' stays 'foobar', 'FOOBAR' becomes 'foobar', and '', '-' and the emoji test case pass through untouched.

Notice that Lowercase<Left> is applied on every step, not just at the end. That's what makes 'HELLO_WORLD_WITH_TYPES' work. Each segment is lowercased as it's emitted, while the character after each underscore is uppercased, yielding 'helloWorldWithTypes'.

Edge cases the tests cover

The whole solution is a single recursive walk: find an underscore, look at one character, decide, repeat. Once you've internalized lazy template literal inference and the single-character infer trick, a whole family of type-level string problems opens up.

This challenge is originally from here.

Share this challenge

Learn the Concepts