This task could be split into two less complex ones. First, to rename object keys prefixing them with full path to the property. And then deep flatten the object.
Renaming keys I believe is the most simple part here:
type RemapKeys<T, D extends number = 5, P extends string ="Object"> =
[D] extends [never] ? never : T extends object ?
{ [K in keyof T as K extends string ? `${P}.${K}` : never]: RemapKeys<T[K], Prev[D], K extends string ? `${P}.${K}` : never> } : T
Here we just keep some prefix P
and when meet any object call RemapKeys
recursively with new prefix consisting of previous P
value and the key of the object we're iterating over. We're using mapped types, key remapping and recursive contidional types here.
After renaming our new type has structure as follows:
interface test {
Ab: { Cd: number, Ef: { Gh: string } };
Ij: boolean;
}
/*
type Remapped = {
"Object.Ab": {
"Object.Ab.Cd": number;
"Object.Ab.Ef": {
"Object.Ab.Ef.Gh": string;
};
};
"Object.Ij": boolean;
}
*/
type Remapped = RemapKeys<test>
Then comes the harder part. Flattening the object.
So the flattened object is the object that consists of the all the properties we have on the root level plus all the properties of the nested objects:
// root level properties
type NonObjectPropertiesOf<T> = {
[K in keyof T as T[K] extends object ? never : K]: T[K]
}
// nested object values
type ValuesOf<T> = T[keyof T];
type ObjectValuesOf<T> = Extract<ValuesOf<T>, object>
But ObjectValuesOf
gives us a union of objects' values. While we need an intersection. That's where the awesome UnionToIntersection
type from @jcalz
comes handy. So, for one-level nested object the Flatten
type could be written as:
type Flatten<T> = NonObjectPropertiesOf<T> & UnionToIntersection<ObjectValuesOf<T>>
/*
type FlattenOneLevelRemapped = {
"Object.Ij": boolean;
} & {
"Object.Ab.Cd": number;
"Object.Ab.Ef": {
"Object.Ab.Ef.Gh": string;
};
}
*/
type FlattenOneLevelRemapped = Flatten<Remapped>
But for deeply nested type we'll need recursion.
type DeepFlatten<T, D extends number = 5> = [D] extends [never] ? never : T extends unknown
? NonObjectPropertiesOf<T> &
UnionToIntersection<DeepFlatten<ObjectValuesOf<T>, Prev[D]>>
: never;
/*
type DeepFlattenRemapped = {
"Object.Ij": boolean;
} & {
"Object.Ab.Cd": number;
} & {
"Object.Ab.Ef.Gh": string;
}
*/
type DeepFlattenRemapped = DeepFlatten<Remapped>
And finally combining it all together:
type IFilterBack<T> = {
Object: DeepFlatten<RemapKeys<T>>
}
interface test {
Ab: { Cd: number, Ef: { Gh: string } };
Ij: boolean;
}
const a: IFilterBack<test> = {
Object: {
"Object.Ab.Cd": 1,
"Object.Ij": true,
"Object.Ab.Ef.Gh": '',
}
};
playground link
I didn't account for properties that can have array type here and not sure this type will scale good enough to fit them.