Since our logic depends on property count, first of all, we need to create an utility type to count the number of object properties/children.
I would say that this is not trivial.
Let's try to do this.
type KeyCount<Obj, Cache extends any[] = []> =
keyof Obj extends never ? Cache['length'] : {
[Prop in keyof Obj]: KeyCount<Omit<Obj, Prop>, [...Cache, Prop]>
}[keyof Obj]
{
type _ = KeyCount<{ a: 'a' }> // 1
type __ = KeyCount<{ a: 'a', b: 'b' }> // 2
type ___ = KeyCount<{ a: 'a', b: 'b', c: 'c' }> // 3
type ____ = KeyCount<{ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f' }> // 6 (MAX supported count of props in TS 4.5.4 )
type _____ = KeyCount<{ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', j: 'j' }> // 7 (works only with TS >= 4.6.* but makes my CPU unhappy)
type ______ = KeyCount<{ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', j: 'j', h: 'h', }> // 8 (works only with TS >= 4.6.*)
}
KeyCount
- recursively iterates through each object property and adds 1
to Cache
array. The length of Cache
coresponds to number of object properties.
Now we need to obtain a union of all possible paths combinations. This is similar to:
See comments for explanation
type Label<Cache extends string, Prop> = Prop extends string ? `${Cache} (${Prop})` : never
type Path<Obj, Cache extends string = ''> =
// If hit the primitive value - return Cache
Obj extends string
? Cache
// Otherswise, iterate through Obj properties and
: {
[Prop in keyof Obj]:
// check if it is first iterated property - don't use Cache as a prefix
Cache extends '' ? Path<Obj[Prop], `${Prop & string}`> :
// if it is not first iterated property, check if number of properties is 1
| KeyCount<Obj> extends 1
// If Obj has only 1 property - return top level property name [parent]
? Cache
// If Obj has more than 1 properties/children - create approperiate label (parent 1 child1)
: Label<Cache, Prop>
| Path<Obj[Prop], Label<Cache, Prop>>
}[keyof Obj]
Whole code
type ParentsWithChildren = {
parent1: {
child1: string,
child2: string;
},
parent2: {
child3: string,
}
}
type KeyCount<Obj, Cache extends any[] = []> =
keyof Obj extends never ? Cache['length'] : {
[Prop in keyof Obj]: KeyCount<Omit<Obj, Prop>, [...Cache, Prop]>
}[keyof Obj]
type Label<Cache extends string, Prop> = Prop extends string ? `${Cache} (${Prop})` : never
type Path<Obj, Cache extends string = ''> =
// If hit the primitive value - return Cache
Obj extends string
? Cache
// Otherswise, iterate through Obj properties and
: {
[Prop in keyof Obj]:
// check if it is first iterated property - don't use Cache as a prefix
Cache extends '' ? Path<Obj[Prop], `${Prop & string}`> :
// if it is not first iterated property, check if number of properties is 1
| KeyCount<Obj> extends 1
// If Obj has only 1 property - return top level property name [parent]
? Cache
// If Obj has more than 1 properties/children - create approperiate label (parent 1 child1)
: Label<Cache, Prop>
| Path<Obj[Prop], Label<Cache, Prop>>
}[keyof Obj]
// "parent1 (child1)" | "parent1 (child2)" | "parent2 (child3)"
type Result = Path<ParentsWithChildren>
Playground
Please keep in mind that calculating number of proeprties is tricky. Maybe it worth using an array for this purpose.
If you don't like my KeyCount
approach, because it has his own recursion limitation, you can consider this approach:
type ParentsWithChildren = {
parent1: {
child1: string,
child2: string;
},
parent2: {
child3: string,
}
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits goes to https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
// NEW VERSION OF KeyCount
type KeyCount<Obj> = UnionToArray<keyof Obj> extends any[] ? UnionToArray<keyof Obj>['length'] : never
type Label<Cache extends string, Prop> = Prop extends string ? `${Cache} (${Prop})` : never
type Path<Obj, Cache extends string = ''> =
Obj extends string
? Cache
: {
[Prop in keyof Obj]:
Cache extends '' ? Path<Obj[Prop], `${Prop & string}`> :
| KeyCount<Obj> extends 1
? Cache
: Label<Cache, Prop>
| Path<Obj[Prop], Label<Cache, Prop>>
}[keyof Obj]
// "parent1 (child1)" | "parent1 (child2)" | "parent2 (child3)"
type Result = Path<ParentsWithChildren>
Playground