#9775β€’Hard

Capitalize Nest Object Keys

Capitalize every key of an object type recursively, following values into nested arrays. The branch order decides whether tuples survive the mapping.

CapitalizeNestObjectKeys<T> capitalizes every key of an object type, and when a value is an array it follows the data and capitalizes the keys of the objects inside it too. The renaming itself is one as clause in a mapped type. The part that earns the hard label is the branch order: arrays are objects, so you have to handle them first.

For example

type Result = CapitalizeNestObjectKeys<{ foo: string, bars: [{ foo: string }] }>
// expected to be { Foo: string, Bars: [{ Foo: string }] }

Challenge Instructions: Capitalize Nest Object Keys

Hard

Capitalize the key of the object, and if the value is an array, iterate through the objects in the array.

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

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:

type CapitalizeNestObjectKeys<T> = T extends readonly unknown[]
  ? { [K in keyof T]: CapitalizeNestObjectKeys<T[K]> }
  : T extends object
    ? {
        [K in keyof T as Capitalize<K & string>]: CapitalizeNestObjectKeys<
          T[K]
        >
      }
    : T

The type is a recursive walker with three branches: one for arrays, one for objects, one for everything else. The order of the branches matters, so I'll take them in order.

Branch 1: arrays and tuples

T extends readonly unknown[] matches arrays and tuples (using readonly here also catches readonly arrays, since mutable arrays are assignable to readonly ones). For these we map without renaming:

[object Object]

This relies on a special behavior: when a mapped type iterates directly over keyof T of a tuple or array, TypeScript preserves the array-ness. Mapping [{ foo: string }] produces another one-element tuple, with the element transformed. Each element then re-enters the recursion, so objects inside the array get their keys capitalized.

This branch has to come first because arrays are objects. If the object branch ran first, it would remap an array's keys, including length, push, map and the numeric indices, and the result would be a mangled object bag instead of a tuple. Checking the more specific shape before the general one is a recurring pattern in conditional types.

Branch 2: plain objects

For non-array objects we use key remapping, the as clause available in mapped types since TypeScript 4.1:

[object Object]

For each key K, the property is stored under the new name computed after as. Two details matter here:

As an intermediate check, applying just this branch to { foo: string } gives:

[object Object]

Branch 3: primitives

Anything that is neither an array nor an object, like the string in our test, falls through to : T and is returned unchanged. This is the recursion's base case. If the type mapped unconditionally instead of checking T extends object first, it would map over keyof string and produce nonsense, so the guard is what lets primitives pass through cleanly.

Tracing the test case

CapitalizeNestObjectKeys<{ foo: string, bars: [{ foo: string }] }>
// object branch: 'foo' β†’ 'Foo' (value string β†’ unchanged)
//                'bars' β†’ 'Bars', value is a tuple β†’ array branch
//   array branch: element { foo: string } β†’ object branch again
//     'foo' β†’ 'Foo'
// result: { Foo: string, Bars: [{ Foo: string }] }

The test asserts strict equality with Equal, so it matters that the tuple stays exactly a one-element tuple, which is what the array-first branch guarantees.

If you've solved Camelize (challenge 1383), you'll recognize the identical skeleton: array branch, object branch with as remapping, primitive fallthrough. Swap the key transformation and you have a reusable template for any "rename all keys recursively" type.

This challenge is originally from here.

Share this challenge

Learn the Concepts