Issue
I've the next data with the type Data
type Data = {
Id: string,
LogicalName: string,
VATRegistered: {
Label: string | null,
Value: number | null,
}
}
const data: Data = {
Id: 'qK1jd828Qkdlqlsz8123assaa',
LogicalName: 'locale',
VATRegistered: {
Label: 'AT401',
Value: 1000001
}
}
And I have to convert it to the next one:
const transformedData = {
Id: 'qK1jd828Qkdlqlsz8123assaa',
LogicalName: 'locale',
VATRegisteredLabel: 'AT401',
VATRegisteredValue: 1000001
}
I've written a function which have to transform my object and return it with the next type
type TransformedData {
Id: string,
LogicalName: string,
VATRegisteredLabel: string | null,
VATRegisteredValue: number | null
}
My function:
const _isNull = (value: any) => {
let res = value === null || undefined ? null : value;
return res
};
function transformData<T extends {}, U extends {}>(obj: T, fatherName: keyof U | undefined) {
let newObj;
for (let key in obj) {
let k = obj[key];
if (typeof k === 'object' && !Array.isArray(k) && k !== null) {
Object.assign(newObj, transformData<typeof k, T>(k, key))
} else {
Object.assign(newObj, { [fatherName ? fatherName + key : key]: _isNull(k) })
}
}
return newObj;
}
But I gain a new object with the empty object type. Is there a way to rewrite the function that it returns a new object with TransformedData type?
Solution
I am interpreting this question as: "How can you take an object type in TypeScript like
type Data = {
Id: string,
LogicalName: string,
VATRegistered: {
Label: string | null,
Value: number | null,
SomethingElse: {
Hello: number
}
}
}
and recursively flatten it to an object type like:
type TransformedData = {
Id: string,
LogicalName: string,
VATRegisteredLabel: string | null,
VATRegisteredValue: number | null,
VATRegisteredSomethingElseHello: number
}
so that all properties are non-object types, and where each key in the new type is the concatenated key path to the resulting property?"
Let me just say that this is possible but brittle and horrifically ugly. TypeScript 4.1 gives you recursive conditional types, template literal types, and key remapping in mapped types, all of which are needed. Conceptually, to Flatten
an object you want to take each property of the object and output them as-is if they are primitives or arrays, and Flatten
them otherwise. To Flatten
a property is to prepend the properties key to the keys of the flattened properties.
This is more or less the approach I take, but there are so many hoops you have to jump through (e.g., avoiding recursion limits, unions-to-intersections, intersections-to-single-objects, avoiding symbol
keys in key concatenation, etc) that it's hard to even begin to explain it in more detail, and there are so many edge cases and caveats (e.g., I'd expect bad things to happen with optional properties, index signatures, or property types which are unions with at least one object type member) that I'd be loath to use such a thing in production environments. Anyway, here it is in all its 🤮 glory:
type Flatten<T extends object> = object extends T ? object : {
[K in keyof T]-?: (x: NonNullable<T[K]> extends infer V ? V extends object ?
V extends readonly any[] ? Pick<T, K> : Flatten<V> extends infer FV ? ({
[P in keyof FV as `${Extract<K, string | number>}${Extract<P, string | number>}`]:
FV[P] }) : never : Pick<T, K> : never
) => void } extends Record<keyof T, (y: infer O) => void> ?
O extends infer U ? { [K in keyof O]: O[K] } : never : never
Then your transformData()
function could be given the following call signature (I'm using an overload and am only concerned about the behavior when you call it with no fatherName
parameter. The rest I'll just give as any
:
function transformData<T extends object>(obj: T): Flatten<T>;
function transformData(obj: any, fatherName: string | number): any
function transformData(obj: any, fatherName?: string | number): any {
let newObj = {};
for (let key in obj) {
let k = obj[key];
if (typeof k === 'object' && !Array.isArray(k) && k !== null) {
Object.assign(newObj, transformData(k, key))
} else {
Object.assign(newObj, { [fatherName ? fatherName + key : key]: _isNull(k) })
}
}
return newObj;
}
Let's see how it works on this data
:
const data: Data = {
Id: 'qK1jd828Qkdlqlsz8123assaa',
LogicalName: 'locale',
VATRegistered: {
Label: 'AT401',
Value: 1000001,
SomethingElse: {
Hello: 123
}
}
}
const transformed = transformData(data);
/* const transformed: {
Id: string;
LogicalName: string;
VATRegisteredLabel: string | null;
VATRegisteredValue: number | null;
VATRegisteredSomethingElseHello: number;
} */
console.log(transformed);
/* {
"Id": "qK1jd828Qkdlqlsz8123assaa",
"LogicalName": "locale",
"VATRegisteredLabel": "AT401",
"VATRegisteredValue": 1000001,
"SomethingElseHello": 123
} */
Hooray, the compiler sees that transformed
is of the same type as TransformedData
even though I didn't annotate it as such. The keys are concatenated in the type as well as the object.
So, there you go. Again, I really only recommend using this for entertainment purposes, as a way of seeing how far we can push the type system. For any production use I'd probably just hardcode a call signature of the type (obj: Data) => TransformedData
if that's what you're using it for, or maybe even stick with any
and just tell people they will need to write their own types when they call it.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.