#1383Hard

Camelize

Rename every snake_case key in an object type to camelCase, recursing through nested objects and tuples. The array branch must come before the object branch.

A backend hands you snake_case JSON and your frontend wants camelCase. Camelize is the type-level half of that adapter: it renames every key of an object type, recursing through nested objects and even objects inside tuples. Two skills combine here: type-level string manipulation to rewrite each key, and key remapping with as to apply the rewrite across the object. It's the type you'd write next to a camelize() runtime function that adapts backend JSON to frontend conventions.

Camelize<{
  some_prop: string, 
  prop: { another_prop: string },
  array: [{ snake_case: string }]
}>
 
// expected to be
// {
//   someProp: string, 
//   prop: { anotherProp: string },
//   array: [{ snakeCase: string }]
// }

Challenge Instructions: Camelize

Hard

Implement Camelize which converts object from snake_case to to camelCase

Camelize<{
some_prop: string,
prop: { another_prop: string },
array: [{ snake_case: string }]
}>
 
// expected to be
// {
//   someProp: string,
//   prop: { anotherProp: string },
//   array: [{ snakeCase: string }]
// }

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

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

Two types, one per sub-problem:

type CamelizeKey<S> = S extends `${infer Left}_${infer Right}`
  ? `${Left}${Capitalize<CamelizeKey<Right>>}`
  : S
 
type Camelize<T> = T extends readonly unknown[]
  ? { [K in keyof T]: Camelize<T[K]> }
  : T extends object
    ? { [K in keyof T as CamelizeKey<K>]: Camelize<T[K]> }
    : T

One converts a single key string, the other applies that conversion across a nested structure. Keeping them separate keeps each one readable.

Sub-problem 1: converting a single key

CamelizeKey rewrites one string. The pattern `${infer Left}_${infer Right}` splits on the first underscore, because template literal inference is lazy: Left matches the shortest prefix it can. Then we glue the pieces back together with the remainder capitalized:

CamelizeKey<'yet_another_prop'>
// Left = 'yet', Right = 'another_prop'
// = `yet${Capitalize<CamelizeKey<'another_prop'>>}`
// = `yet${Capitalize<'anotherProp'>}`
// = 'yetAnotherProp'

The recursion first resolves the tail ('another_prop''anotherProp'), then Capitalize uppercases its first letter. Keys without an underscore don't match the pattern and fall through unchanged. That's both the recursion's base case and the reason prop or array stay as they are. Note that S is left unconstrained so we can feed it keyof T directly, which may include number or symbol keys; non-strings take the false branch.

Sub-problem 2: walking the structure

Camelize has three branches, and their order matters.

Arrays and tuples first. T extends readonly unknown[] catches tuples like [{ snake_case: string }]. Here we use a plain mapped type without key remapping:

[object Object]

Mapping over a tuple this way is a special TypeScript behavior: the result is still a tuple of the same length, with each element type transformed. We must not rename anything here: a tuple's keys include length and the numeric indices, and remapping them would destroy the array structure. That's why arrays get their own branch before the object branch.

Then plain objects. T extends object handles the interesting case using key remapping:

[object Object]

The as clause is the modern (TS 4.1+) way to rename keys while mapping: for each key K, the property lands under the new name CamelizeKey<K> instead of K. The value side recurses with Camelize<T[K]>, which is what converts prop: { another_prop: string } into prop: { anotherProp: string }. The outer key has no underscore, but the nested object still gets processed.

Finally, everything else. Primitives like string don't match either branch and are returned untouched. This is the recursion's stopping point at the leaves.

Tracing the test case

For the array property in the example above, Camelize hits the tuple branch and maps over the tuple's elements. Each element (an object) then flows through the object branch, renaming snake_casesnakeCase. The actual test suite goes further. Its tuple holds several elements, including one with a nested { yet_another_prop: string } that becomes { yetAnotherProp: string } two levels deep. Structure preserved, every key at every depth converted.

Edge cases worth noting

The split into a key-level helper plus a structure-level walker is a pattern you'll reuse constantly: whenever a hard challenge transforms both names and shapes, solve the two layers separately.

This challenge is originally from here.

Share this challenge

Learn the Concepts