Yuzhe's Blog

yuzhes

CamelCase

CamelCase

Challenge Link

Challenge

Implement CamelCase<T> which converts a snake_case string into camelCase.

type camelCase1 = CamelCase<'hello_world_with_types'>
// expected: 'helloWorldWithTypes'

type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'>
// expected: 'helloWorldWithTypes'

Solution

type CamelCase<S extends string> =
  S extends `${infer Head}_${infer C}${infer Tail}`
    ? `${Lowercase<Head>}${Uppercase<C>}${CamelCase<Tail>}`
    : Lowercase<S>

The key idea: split at each underscore into three parts — lowercase what’s before it, uppercase the character right after it, and recurse on the rest.

Breaking it down

Step 1 — Three-part pattern match at underscore

S extends `${infer Head}_${infer C}${infer Tail}`

Template literal inference splits the string at the first _ into three parts:

For 'hello_world_with_types': Head = 'hello', C = 'w', Tail = 'orld_with_types'

Step 2 — Transform and recurse

`${Lowercase<Head>}${Uppercase<C>}${CamelCase<Tail>}`

Step 3 — Base case

: Lowercase<S>

When there’s no underscore left, just lowercase the remaining segment.

Walkthrough:

S = 'hello_world_with_types'
Head = 'hello', C = 'w', Tail = 'orld_with_types'
→ 'hello' + 'W' + CamelCase<'orld_with_types'>

S = 'orld_with_types'
Head = 'orld', C = 'w', Tail = 'ith_types'
→ 'orld' + 'W' + CamelCase<'ith_types'>

S = 'ith_types'
Head = 'ith', C = 't', Tail = 'ypes'
→ 'ith' + 'T' + CamelCase<'ypes'>

S = 'ypes' (no underscore)
→ 'ypes'

Final: 'hello' + 'W' + 'orld' + 'W' + 'ith' + 'T' + 'ypes'
     = 'helloWorldWithTypes' ✓

For uppercase input 'HELLO_WORLD_WITH_TYPES':

Head = 'HELLO' → Lowercase → 'hello'
C = 'W' → Uppercase → 'W'
Tail = 'ORLD_WITH_TYPES' → recurse...
→ 'hello' + 'W' + 'orld' + 'W' + 'ith' + 'T' + 'ypes'
= 'helloWorldWithTypes' ✓

Deep Dive

Why three-part inference ${Head}_${C}${Tail}?

The critical insight is inferring a single character C right after the underscore. This is more precise than splitting into just two parts:

This avoids the common pitfall where recursive Lowercase destroys the Capitalize applied in a previous recursion level.

Why Lowercase<Head> is needed

Without lowercasing, all-caps input like 'HELLO_WORLD' would become 'HELLOWorld' instead of 'helloWorld'. The Lowercase<Head> normalizes each segment before the underscore.

Edge cases

Consecutive underscores: 'foo__bar'

Leading underscore: '_foo_bar'

No underscores: 'hello'

Alternative: two-part split approach

A common first attempt uses two-part splitting:

// ❌ Broken: Lowercase destroys previous Capitalize
type CamelCase<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Lowercase<Head>}${CamelCase<Capitalize<Lowercase<Tail>>>}`
    : Lowercase<S>

This fails because when recursing on 'World_with_types', the Lowercase<Head> turns 'World' back to 'world', losing the capitalization applied by the previous level’s Capitalize. The three-part split avoids this entirely.

Template literal types are Turing-complete

This challenge showcases that TypeScript’s template literal types, combined with recursion and conditional types, form a Turing-complete string processing system. You can implement virtually any string transformation at the type level.

Key Takeaways