#20744Easy

Overwrite Type With Type Of Child

Flatten a nested data property into its parent object type at every depth, using Omit, an intersection and a mapped type that rebuilds one flat result.

API responses love to wrap the interesting fields in a data envelope. This challenge unwraps them in the type system.

ReducedNesting<T> walks an object type and, wherever a property is an object with a data key, lifts the contents of data up one level and drops the data key itself. The rewrite has to happen at every depth, so a data buried three objects down gets the same treatment as one at the top.

interface Model {
  id: number
  extension: {
    id: number
    data: {
      id: number
      prop1: any
      prop2: any
    }
  }
}
 
// ReducedNesting<Model> is expected to be
type ReducedModel = {
  id: number
  extension: {
    id: number
    prop1: any
    prop2: any
  }
}

Challenge Instructions: Overwrite Type With Type Of Child

Easy

Assuming you have an object of type

{
id: number
foo: {
id: number
data: {
id: number
prop1: any
prop2: any
}
}
}

and we want to remove the nesting of the data to get a type

{
id: number
foo: {
id: number
prop1: any
prop2: any
}
}

Should also work with deeper levels of nesting e.g.

interface DeepModel {
id: number
foo: {
id: number
data: {
id: number
prop1: any
prop2: any
bar: {
id: number
data: {
id: number
prop1: any
prop2: any
}
}
}
}
}

should be reduced to

interface ReducedDeepModel {
id: number
foo: {
id: number
prop1: any
prop2: any
bar: {
id: number
prop1: any
prop2: any
}
}
}

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

Change the following code to make the test cases pass (no type check errors).

Loading...

Detailed Explanation

The solution splits into two types, one per job:

type Inline<T> = T extends { data: infer D } ? Inline<Omit<T, 'data'> & D> : T
 
type ReducedNesting<T> = T extends object
  ? { [K in keyof Inline<T>]: ReducedNesting<Inline<T>[K]> }
  : T

Inline fixes a single object. ReducedNesting applies it everywhere.

Lifting one level

Start with the object that actually needs work:

[object Object]

T extends { data: infer D } is a conditional type with an inference slot. It asks "does T have a data property?", and if it does, D is bound to whatever that property holds. Here D becomes { id: number; prop1: any; prop2: any }.

Now build the replacement. Omit<T, 'data'> is everything except the envelope key, and intersecting it with D adds the lifted members back in:

type WithoutData = Omit<Leaf, 'data'> // { id: number }
type Lifted = WithoutData & { id: number; prop1: any; prop2: any }
type Keys = keyof Lifted // 'id' | 'prop1' | 'prop2'

Inline then calls itself on that result. If D had carried its own data key, that key is now sitting at the top of Lifted and the next pass removes it too. The recursion stops as soon as the conditional fails, which is the moment there is no data key left, so Inline<{ id: number }> returns immediately.

Turning the intersection back into an object

Lifted is correct but it is an intersection, and Equal in the tests compares types structurally rather than by what they simplify to on screen. { id: number } & { id: number; prop1: any; prop2: any } is not the same type as { id: number; prop1: any; prop2: any } as far as that check is concerned.

A mapped type solves it. Iterating over keyof Inline<T> and indexing back into Inline<T> produces a fresh object type with those three keys and nothing else:

{ [K in keyof Inline<T>]: Inline<T>[K] }
// { id: number; prop1: any; prop2: any }

Indexing does the merging for you. Inline<Leaf>['id'] is number & number, which collapses to number.

Recursing into children

The property values need the same treatment, so instead of Inline<T>[K] the mapped type writes ReducedNesting<Inline<T>[K]>. That is where the deep case gets handled: foo is flattened, then each of its properties goes through the whole process again, so bar loses its own data wrapper on the next turn.

The outer T extends object guard is what stops the recursion at leaves. ReducedNesting<number> finds no object, takes the false branch, and hands number straight back. Without that guard the mapped type would try to run over a primitive.

Reading the two types together for Model: Inline<Model> is Model itself, because the top level has no data key. The mapped type then visits id (a primitive, returned as-is) and extension, and ReducedNesting<extension> is the Leaf walkthrough above.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges