jeanpaul62
jeanpaul62

Reputation: 10591

Object.keys() with nested objects: get correct types

I have 3 objects a, b and c, each of them has a prop field, whose value can be anything.

const a = { prop: { foo: 'bar', bar: true } }
const b = { prop: { baz: 1234 } }
const c = { prop: true }

These objects are organized into sections:

const sectionA = { a, b }
const sectionB = { c }
const allSections = {sectionA, sectionB}

I want to construct the following object dynamically:

const myObject = {
  sectionA: {
    a: {foo: 'bar', bar: true},
    b: {baz: 1234}
  },
  sectionB: {
    c: true
  }
}

I.e. correctly organized into nested objects, but without the prop field.

Here's what I did:

type ObjectWithProp = { prop: any }

type ObjectTypes<Section extends {[index:string]: ObjectWithProp}> = {
    [K in keyof Section]: Section[K]['prop']
}

type AllSectionTypes<AllSection extends { [index: string]: { [index: string]: ObjectWithProp } }> = {
    [K in keyof AllSection]: ObjectTypes<AllSection[K]>
}

const result = Object.keys(allSections).reduce((outerAcc, _key) => {
    const key = _key as keyof typeof allSections;
    const section = allSections[key];

    outerAcc[key] = Object.keys(section).reduce((innerAcc, _objectName) => {
        const objectName = _objectName as keyof typeof section;
        const obj = section[_objectName];

        innerAcc[objectName] = obj.prop;

        return innerAcc;
    }, {} as ObjectTypes<typeof section>);

    return outerAcc;
}, {} as AllSectionTypes<typeof allSections>)

[Link to TS Playground]

At line const objectName = _objectName as keyof typeof section;, objectName is unfortunately never (logical since there are no common fields between the objects). But then I cannot do innerAcc[objectName].

How can I solve that?

Upvotes: 2

Views: 77

Answers (1)

jcalz
jcalz

Reputation: 330436

Note: the following works in TS3.4+ as it relies on higher order type inference from generic functions.

Here's how I'd go about it. It's a fairly sizable refactor, mostly because all the nested types were hurting my brain. You can probably copy the types from this into your original code, but I wouldn't try to do it. The actual behavior, though, is pretty much the same (e.g., using Object.keys().reduce() instead of a for loop):

// a technically unsafe version of Object.keys(o) that assumes that 
// o only has known properties of T
function keys<T extends object>(o: T) {
  return Object.keys(o) as Array<keyof T>;
}

// Turn {k1: {prop: v2}, k3: {prop: v4} into {k1: v2, k3: v4}
function pullOutProp<TK extends Record<keyof TK, { prop: any }>>(o: TK) {
  return keys(o).reduce(
    <P extends keyof TK>(
      acc: { [P in keyof TK]: TK[P]['prop'] },
      k: P
    ) => (acc[k] = o[k].prop, acc),
    {} as { [P in keyof TK]: TK[P]['prop'] });
}

// Turn {k1: {k2: {prop: v3}, k4: {prop: v5}}, k6: {k7: {prop: v8}}} into
// {k1: {k2: v3, k4: v5}, k6: {k7: v8}}
function nestedPullOutProp<T extends { 
  [K in keyof T]: Record<keyof T[K], { prop: any }> }
>(o: T) {
  return keys(o).reduce(
    <K extends keyof T>(
      acc: { [K in keyof T]: { [P in keyof T[K]]: T[K][P]['prop'] } },
      k: K
    ) => (acc[k] = pullOutProp(o[k]), acc),
    {} as { [K in keyof T]: { [P in keyof T[K]]: T[K][P]['prop'] } }
  )
}

const result = nestedPullOutProp(allSections); // your desired result

You can verify that result is the type you expect. The trick here is basically to make pullOutProp() and nestedPullOutProp() as generic as possible, operating over a type with the minimum requirements to function (e.g., T extends { [K in keyof T]: Record<keyof T[K], { prop: any }> }> means that t.x.y.prop will exist whenever t.x.y exists), and making the callback to reduce() generic. At each step, the generic types are straightforward enough for the compiler to follow the logic and not complain about the assignments... especially because of the improvements made to higher order type inference in generic functions introduced in TS3.4.

Only at the very end, when you call nestedPullOutProp(allSections) does the compiler actually go ahead and try to evaluate the generics, at which point it becomes the expected type.

Okay, hope that helps; good luck!

Link to code

Upvotes: 1

Related Questions