Reputation: 10591
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>)
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
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!
Upvotes: 1