0

Given this record:

type SomeRecord = {a: {a1: 'a1', a2: 'a2'}, b: {b1: 'b1'}}

How to only derive the paths that point to scalars, so that we may end up with:

type RecordPathsThatPointToScalars  
    = ['a', 'a1']
    | ['a', 'a2']
    | ['b', 'b1'];

A scalar may be:

type Scalar = string | number | boolean
Daniel Birowsky Popeski
  • 8,752
  • 12
  • 60
  • 125
  • 1
    What is a "scalar" here? Does `Leaves` from [the answer I pointed you at before](https://stackoverflow.com/a/58436959/2887218) help? – jcalz Jul 21 '20 at 20:17
  • I unfortunately, cannot understand the `Leaves` type, but when I say scalar, I mean `string | number | boolean`. – Daniel Birowsky Popeski Jul 21 '20 at 22:42
  • `Paths` gives you all the paths from the root of the tree to any node in the tree, while `Leaves` gives you only the paths from the root of the tree that end in a *leaf* node. So if "non-object" is good enough, then `Leaves` will work for you. Like this: – jcalz Jul 22 '20 at 01:51
  • [playground link](https://www.typescriptlang.org/play/#code/C4TwDgpgBAyg9gWwgJQgYzgJwCZQLxQDeUAhgFxGkCMFA5CVbQDSkBMdJrtUAviwEYVi-GlFojuPXgFgAUKEiw0JADYlMABRLAAFgGd8UADIQSANwh6APPCSoMOAHwBuKAHo3UANoAiEj5Y-Kh8AXSgAH28-AKg-VlCIqP4YnxFQuTkFaABhOAA7awAJFgAVR0MSqAgAD2AIPOwDTFNsfJUQUjyQLzCAfigACgGdCmKoADpJ4AoSgEp8crM4AEtseZq6hoMhyfHMCmW8gDMITChkebxFlbWofuQoCjyICzOnl9PnTPBoDWazQxeZ6vFgABhYVBYrBYAGYWAAWFgAVhYADYWAB2FgADhYAE4IeCoFRIcTocS4RNJqCeiFvooTOZLFYSiwACJVWr1RpQPIAVwQ-FOhiooPKBC8bLCG25BiBH0wfV5CseUEqMq2UDg-AAVuhgHdKF4ANJQQ5QADWEBAcCOapCAFpehRcgUrMaWIyLNYSiaQiw-i9JSFHOUeF4rTa7SUwhQel9ZHIgA) – jcalz Jul 22 '20 at 01:53
  • If you need `string | number | boolean` specifically can you show an example where there are some non-object properties you want to exclude? Like `{foo: string, bar: symbol, baz: undefined, qux: null}` you *only* want `["foo"]`? – jcalz Jul 22 '20 at 01:56
  • @jcalz exactly! I only want leaves that satisfy that scalar condition, so your example is spot on. Ideally, if possible, you could come up with a type that accepts the condition as an argument. – Daniel Birowsky Popeski Jul 22 '20 at 07:11

1 Answers1

1

My other answer mentions this but I will reiterate: this sort of type function is recursive in a way that isn't really supported by TypeScript. It works... until it doesn't work (e.g., the compiler gets bogged down or reports circularity errors). So I don't really recommend using this in any production code base.

Anyway, I can modify the other answer's Paths<T> definition to be Paths<T, V> which gives a union of tuples representing key paths in an object of type T where the value pointed to by that path is assignable to type V. So Paths<T, unknown> or Paths<T, any> should give all the paths, while Paths<T, string | number | boolean> should give you paths that point to "scalar" values.

Here it is:

type Cons<H, T> = T extends readonly any[] ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
    : never;

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]

type Paths<T, V = unknown, D extends number = 10> = [D] extends [never] ? never :
    (T extends V ? [] : never) | (T extends object ? {
        [K in keyof T]-?: Cons<K, Paths<T[K], V, Prev[D]>>
    }[keyof T] : never);

The idea is that it walks down through the object (up to some maximum depth of 10 or so, by default), and collects all the paths, but only outputs ones where the type is assignable to V.

If SomeRecord is this:

type SomeRecord = {
    a: { a1: 'a1', a2: 'a2' }, b: { b1: 'b1' },
    c: { foo: string, bar: symbol, baz: undefined, qux: null, quux: () => void }
}

(where I've added a c with some properties, most of which are not "scalars" so they should be excluded,) then the full paths are:

type AllPaths = Paths<SomeRecord>;
// type AllPaths = [] | ["a"] | ["a", "a1"] | ["a", "a2"] | ["b"] | ["b", "b1"] | 
//  ["c"] | ["c", "foo"] | ["c", "bar"] | ["c", "baz"] | ["c", "qux"] | ["c", "quux"]

and the scalar paths are:

type ScalarPaths = Paths<SomeRecord, string | number | boolean>;
// type ScalarPaths = ["a", "a1"] | ["a", "a2"] | ["b", "b1"] | ["c", "foo"];

Looks like what you want, I think.


Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360