0

Given an object type SomeRecord = {a: {a1: 'a1', a2: 'a2'}, b: {b1: 'b1'}}, I want a type Path<T extends Record<string, any>> that represents all possible record paths as tuples:

// Path<SomeRecord> will be a union of:
['a']
['a', 'a1']
['a', 'a2']
['b']
['b', 'b1']
// And nothing else

I read a ton of suggestions where the path is a parameter of a function. That doesn't work for me because I only need the tuples.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
Daniel Birowsky Popeski
  • 8,752
  • 12
  • 60
  • 125
  • The duplicated answer has a `Paths` that works this way, although it's not supported. Your case would work like... (next comment) – jcalz Jul 21 '20 at 16:28
  • like [this](https://www.typescriptlang.org/play/#code/C4TwDgpgBAwg9gOwM4B4ASAaKAVAfFAXhyggA9gIEATJKAJwgEMrEAbEKRhEAbQF0oAfgCwAKCgSoACikALAFxRMUAHRrgi7AEpC+AG5wAllR1kK1WjLUq6iwwgBmEOlABKOgvqMmhbqIoQIPWcxSX8oQOC6AG4xUEgoAAUGPUIoHkjnLAAGLABGLAAmLABmLAAWLABWLAA2LAB2LAAOLABOfNyoPILu4u6y7srumu767qbu1u6OqEKu62z+PjjwaETGYFlUbCwAERJyShoIgFcAWwAjZzS87PwiHj2BM2PaDKDnAUEIz5dNQ7mE5wS4AKwgAGNgEJQpIAN7pADSUHsUAA1hAQHAHDg+ABaQSKHiIgQAH2kGy2O2JfCwySCTz4+FeFhRjhuiRh4jCkk5LJO-F8mX+sEQqERdPwAT+sIkOgAvjwMVicdgVtyJES+LFRPFoABlODnCCuSFwOhUNJwxiKa15RQAckYeQdWEYhUd7od8qwl1tl3tUAdAe98tWCQAsiBKds0jHUIbjaaIeaqLhokA). – jcalz Jul 21 '20 at 16:28
  • @jcalz ahh, that's too bad. As my question is way more concise : ) Thanx for the answer! – Daniel Birowsky Popeski Jul 21 '20 at 17:25

1 Answers1

3

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"]

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357