I'm attempting to express type to a function that takes an object with keys as dot delimited paths and returns a new object, where all sub-paths are expanded. I can't wrap my head around how would one preserve the original type for a given path while recursively 'de-structuring' them.
// Dummy structure
const flattenedObject = {
'd.a': 1,
'd.b': true,
};
type FlattenedObject = typeof flattenedObject;
// Expected type (or union of sub-paths: `{ d: { a: number } } | { d: { b: boolean } }`)
interface NestedObject {
d: {
a: number;
b: boolean;
}
}
// Attempt
type MapNestedKeys<R, N extends keyof R = keyof R, N0 = N> = N0 extends string
? N0 extends `${infer K1}.${infer K2}`
? { [K in N as `${K1}`]: MapNestedKeys<R, N, K2> }
: { [K in N as N0]: R[K] }
: {};
// Gives union, but types for nested keys are now union on all key types
type Test = MapNestedKeys<FlattenedObject>;
An alternative approach that works a bit better with various depths of the path (e.g. 'd.a.c')
type MapNestedKeys<T, P = keyof T> =
P extends `${infer K1}.${infer K2}` & keyof T ?
MapNestedKeys<{[S in K1]: MapNestedKeys<{[S1 in K2]: T[P]}>}> :
P extends string & keyof T ? {[S in P]: T[P]} : {}