2

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?

3
  • The code in your link doesn't match the code here, at least not exactly. Commented May 31, 2019 at 15:31
  • I'm thinking you're going to need to assert your way out of this, unfortunately. Even if you can represent the callback functions in reduce as generic in the key K of typeof allSections and the key P of typeof allSections[K], the compiler can't seem to reason about typeof allSections[K][P] and how it relates to AllSectionTypes<typeof allSections>[K][P]. Commented May 31, 2019 at 16:01
  • @jcalz Link fixed. Assert is fine (as long as it's not as any), how would you assert? Commented May 31, 2019 at 16:06

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.