I want to convert a structure type into another. The source structure is an object which possibly contains properties keys splitted by a dot. I want to expand those "logic groups" into sub-objects.
Hence from this:
interface MyInterface {
'logicGroup.timeout'?: number;
'logicGroup.serverstring'?: string;
'logicGroup.timeout2'?: number;
'logicGroup.networkIdentifier'?: number;
'logicGroup.clientProfile'?: string;
'logicGroup.testMode'?: boolean;
station?: string;
other?: {
"otherLG.a1": string;
"otherLG.a2": number;
"otherLG.a3": boolean;
isAvailable: boolean;
};
}
to this:
interface ExpandedInterface {
logicGroup: {
timeout?: number;
serverstring?: string;
timeout2?: number;
networkIdentifier?: number;
clientProfile?: string;
testMode?: boolean;
}
station?: string;
other?: {
otherLG: {
a1: string;
a2: number;
a3: boolean;
},
isAvailable: boolean;
};
}
__
(EDIT) These two structures above are based, of course, on a real Firebase-Remote-Config Typescript transposition that cannot have arbitrary props (no index signatures) or keys collision (we don't expect to have any logicGroup
and logicGroup.anotherProperty
).
Properties can be optional (logicGroup.timeout?: number
) and we can assume that if all the properties in logicGroup
are optional, logicGroup
itself can be optional.
We do expect that an optional property (logicGroup.timeout?: number
) is going to maintain the same type (logicGroup: { timeout?: number }
) and not to become mandatory with the possibility to explicitly accept undefined
as a value (logicGroup: { timeout: number | undefined }
).
We expect all the properties to be objects, strings, numbers, booleans. No arrays, no unions, no intersections.
I've tried sperimenting a bit with Mapped Types, keys renaming, Conditional Types and so on. I came up with this partial solution:
type UnwrapNested<T> = T extends object
? {
[
K in keyof T as K extends `${infer P}.${string}` ? P : K
]: K extends `${string}.${infer C}` ? UnwrapNested<Record<C, T[K]>> : UnwrapNested<T[K]>;
}
: T;
Which doesn't output what I want:
type X = UnwrapNested<MyInterface>;
// ^? ===> { logicGroup?: { serverString: string | undefined } | { testMode: boolean | undefined } ... }
So there are two issues:
- logicGroup is a distributed union
- logicGroup properties hold
| undefined
instead of being actually optional.
So I tried to prevent the distribution by clothing K
value:
type UnwrapNested<T> = T extends object
? {
[
K in keyof T as K extends `${infer P}.${string}` ? P : K
]: [K] extends [`${string}.${infer C}`] ? UnwrapNested<Record<C, T[K]>> : UnwrapNested<T[K]>;
}
: T;
This is the output, but it is still not what I want to get:
type X = UnwrapNested<MyInterface>;
// ^? ===> { logicGroup?: { serverString: string | boolean | number | undefined, ... }}
One problem goes away and another raises. So the issues are:
- All the sub properties get as a type a union of all the values available in the same "logic group"
- All the sub properties hold
| undefined
instead of being actually optional.
I've also tried to play with other optionals in the attempt of filtering the type and so on, but I don't actually know what I am missing.
I also found https://stackoverflow.com/a/50375286/2929433, which actually works on its own, but I wasn't able to integrate it inside my formula.
What I'm not understanding? Thank you very much!