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']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.
The entire solution:
type Path<T> = T extends object
? {
[Key in keyof T]: [Key] | [Key, ...Path<T[Key]>]
}[keyof T]
: neverFive 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.
T extends objectEvery 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.
[Key] | [Key, ...Path<T[Key]>]For each key of the current object, a valid path either stops here or continues deeper:
[Key]: the path that ends at this key.[Key, ...Path<T[Key]>]: this key followed by any valid path through the value at that key. T[Key] is an indexed access ("give me the type of this property"), and the spread splices the recursive result onto the tail of the tuple.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.
{ ... }[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.
Path<{ a: string }> accepts ['a'] but rejects ['z']: the union only ever contains keys that actually exist, so invalid paths aren't in it.Path<{ b: number; c: number }> accepts both ['b'] and ['c']: sibling keys each contribute their own branch to the union.['baz', 'b'] work because each recursion level prepends exactly one key via the tuple spread.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.