Before we start, not that generating a union of all possible paths might be expensive. in terms of compilation time. Also the solution below uses recursive conditional types, which have their own perf issues and their use is not encouraged.
The most elegant solution is to use the new TS feature in 4.0 (unreleased yet) that allows spreading of tuples in other tuples, as described here. With this and with a recursive conditional type we can create a all the possible tuple paths:
type SomeRecord = { a: { a1: 'a1', a2: 'a2' }, b: { b1: 'b1', b2: { b21: string, b22: string, } } }
type Paths<T, K extends keyof T = keyof T> = K extends K ? [K, ...{
0: []
1: [] | Paths<T[K]>
}[T[K] extends string | number | boolean ? 0: 1]] : never;
type x = Paths<SomeRecord>
Playground Link
The way it works, is we take each key in T
, and use a distributive conditional type to take each key K
and create a tuple, where K
is the first item, followed by a spread of:
- either the empty tuple (
[]
) if T[K]
has no other keys we are interested in (ie is a primitive) o
- or a tuple made from
[]
(in order to allow just [K]
) in union with the paths of the type of T[K]
.
Unormalized the result would be something like ['a', ...([] | ['a1'] | ['a2'])] | ['b', ...([] | ['b1'] | ['b2', ...([] | ['b21'] | ['b22'] ])]
. Fortunately the compiler will normalize that monstrosity to ["a"] | ["a", "a1"] | ["a", "a2"] | ["b"] | ["b", "b1"] | ["b", "b2"] | ["b", "b2", "b21"] | ["b", "b2", "b22"]