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 oneImplement 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 oneView 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.
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.
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.
Uppercase<Ch> extends Lowercase<Ch> looks strange at first, but it answers a useful question: does this character have a case at all?
'b': Uppercase<'b'> is 'B' and Lowercase<'b'> is 'b'. They differ, so the check is false.'_' or '$': uppercasing and lowercasing change nothing, so both sides are identical and the check is true.So the condition is true exactly when Ch is not a cased letter.
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'.
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'.
'foo_bar_' β 'fooBar_': the trailing underscore has no character after it, so the three-part pattern doesn't match that tail and it's returned as-is by the base case.'foo_$bar' β 'foo_$bar': $ is uncapitalizable, so the underscore is kept and $bar continues unchanged.'foo_bar__' β 'fooBar__': the first underscore merges into B; the final two underscores fall into the "keep the underscore" branch and the base case.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.