I have a mapper which takes in any object, and if there is a key named 'fields' in it, it's replaced by its contents. So for example,
const obj = { x: 'hey', fields: { y: 'you' } }
const mapped = contentfulMapper(obj);
// mapped = { x: 'hey', y: 'you' }
Or
const givenObj = {
a: "sdf",
w: {
a2: 343,
a3: true,
fields: {
a4: "ll",
q: {
fields: {
a5: [2, 4, "lk"]
},
},
},
},
};
const mapped = contentfulMapper(givenObj);
/* mapped = {
a: "sdf",
w: {
a2: 343,
a3: true,
a4: "ll",
q: {
a5: [2, 4, "lk"]
},
},
}; */
Returning a Record<string, unknown> isn't great though, so my question is how can I type this correctly?
type TContentfulModel = {
fields?: undefined | Record<string, TContentfulModel>;
};
const contentfulMapper = (
obj: Record<string, TContentfulModel | unknown>
) => {
let out: Record<string, unknown> = {};
Object.keys(obj).forEach((key) => {
const field = obj[key] as TContentfulModel;
if (key === 'fields') {
const mappedField = contentfulMapper(field);
out = { ...out, ...mappedField };
} else {
out[key] =
typeof obj[key] === 'object' && !Array.isArray(obj[key])
? contentfulMapper(field)
: obj[key];
}
});
return out;
};
Here is a working solution. Please go through it, let me know if don't get anything. I will add any needed explanation, then.
type TContentfulModelExtended = {
fields: TContentfulModelExtended | TContentfulModel
[key: string]: any
}
type TContentfulModel = {
[key: string]: any
}
// if `T` has `fields`, we omit that key, keep rest,
// and take it's intersection with the flatten nested fields
type Flat<T extends TContentfulModelExtended | TContentfulModel> = T extends TContentfulModelExtended
? Omit<T, 'fields'> & Flat<T['fields']>
: T
type MyObjType = {
a: string
fields: {
b: number
c: boolean
fields: {
d: number[],
fields: {
e: Promise<number>
}
}
}
}
type Flattened = Flat<MyObjType>
const obj: Flattened = {
a: 'abc',
b: 3,
c: false,
d: [3],
e: Promise.resolve(3)
}
const contentfulMapper = <O extends TContentfulModelExtended | TContentfulModel>(obj: O) => {
let out: Flat<O> = {} as any;
Object.keys(obj).forEach((key) => {
const field = obj[key];
if (key === 'fields') {
// @ts-ignore
const mappedField = contentfulMapper(field);
out = { ...out, ...mappedField };
} else {
out[key] =
typeof obj[key] === 'object' && !Array.isArray(obj[key])
? contentfulMapper(field)
: obj[key];
}
});
// @ts-ignore
return out;
};
const givenObj = {
a: 'sdf',
fields: {
a2: 343,
a3: true,
fields: {
a4: 'll',
fields: {
a5: [2, 4, 'lk']
}
}
}
}
const objSpreaded = contentfulMapper(givenObj)
objSpreaded.a // all props from `a` to `e` works properly
objSpreaded.a2
objSpreaded.a3
objSpreaded.a4
objSpreaded.a5
objSpreaded.f // expected error
Please note, I have added compiler directive comments // @ts-ignore to get rid of infinite depth error.