#15260Hard

Tree path array

Path<T> computes every valid key path through a nested object as a union of tuples. The machinery behind type-safe lodash.get and form field paths.

Path<T> turns a nested object type into the union of every valid key path, written as tuples.

Type-safe versions of lodash.get and form libraries that validate ['address', 'street']-style field paths both run on this exact machinery. Given a tree-shaped type, Path produces the union of all tuples that walk from the root toward a leaf.

declare const example: {
  foo: {
    bar: {
      a: string
    }
    baz: {
      b: number
      c: number
    }
  }
}
 
// All valid paths:
// [] (not required by the tests; see note below)
// ['foo']
// ['foo', 'bar']
// ['foo', 'bar', 'a']
// ['foo', 'baz']
// ['foo', 'baz', 'b']
// ['foo', 'baz', 'c']

Challenge Instructions: Tree path array

Hard

Create a type Path that represents validates a possible path of a tree under the form of an array.

Related challenges:

declare const example: {
foo: {
bar: {
a: string;
};
baz: {
b: number
c: number
}
};
}
 
// Possible solutions:
// []
// ['foo']
// ['foo', 'bar']
// ['foo', 'bar', 'a']
// ['foo', 'baz']
// ['foo', 'baz', 'b']
// ['foo', 'baz', 'c']

View on GitHub: https://tsch.js.org/15260

Change the following code to make the test cases pass (no type check errors).

👋 Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The entire solution:

type Path<T> = T extends object
  ? {
      [Key in keyof T]: [Key] | [Key, ...Path<T[Key]>]
    }[keyof T]
  : never

Five lines that generate an entire tree of tuples. There are three moving parts: a recursion guard, a mapped type used as a builder, and an indexed access that collapses the map into a union.

The recursion guard: T extends object

Every recursion needs a base case. Here we stop descending when we hit a leaf value like string or number, something with no keys worth walking into. T extends object is that gate. For Path<string> the condition fails and the result is never. Keep that never in mind; it does useful work in a moment.

Building one level: [Key] | [Key, ...Path<T[Key]>]

For each key of the current object, a valid path either stops here or continues deeper:

There's a subtlety hiding in the spread: Path<T[Key]> is usually a union of tuples, and spreading a union into a tuple distributes over it. So if Path<T['baz']> is ['b'] | ['c'], then ['baz', ...Path<T['baz']>] becomes ['baz', 'b'] | ['baz', 'c']: one tuple per branch, which is what we want.

And what about leaves? For T = { a: string }, the deeper branch is ['a', ...Path<string>] = ['a', ...never]. Spreading never into a tuple annihilates the whole tuple (it evaluates to never), and ['a'] | never simplifies to ['a']. The base case silently prunes the impossible "keep going past a leaf" branch, no special-casing required.

Collapsing the map: { ... }[keyof T]

The mapped type on its own produces an object whose property values are path unions:

// For the inner { bar: ..., baz: ... } object:
// {
//   bar: ['bar'] | ['bar', 'a']
//   baz: ['baz'] | ['baz', 'b'] | ['baz', 'c']
// }

We don't want that object. We want everything in it, merged. Indexing a type by a union of its keys returns the union of the corresponding value types, so {...}[keyof T] flattens the map into:

[object Object]

This { [K in keyof T]: ... }[keyof T] shape, compute something per key and then union the results, comes up all over type-level code and is worth memorizing on its own.

Edge cases the tests cover

One honest limitation to know about: the challenge's comment lists [] (the empty path) as valid, but the tests only exercise non-empty paths, and this solution produces non-empty tuples only. If you needed the empty path too, you'd union a [] onto the result. As written, the solution matches what the test suite demands and stays as small as possible.

This challenge is originally from here.

Share this challenge

Learn the Concepts