Patterns
How do I read `user.address.city` through a reusable path without losing the result type?
Generate the valid key tuples recursively, then consume one key at a time to compute the value at the end.
The recipe
type Paths<T> = T extends object ? { [K in keyof T]-?: [K] | [K, ...Paths<T[K]>] }[keyof T] : [] type PathValue<T, P extends readonly PropertyKey[]> = P extends readonly [infer K, ...infer Rest] ? K extends keyof T ? Rest extends readonly PropertyKey[] ? PathValue<T[K], Rest> : never : never : T function getPath<T, const P extends readonly PropertyKey[]>( value: T, ...path: P & Paths<T> ): PathValue<T, P> { let current: unknown = value for (const key of path) { current = (current as Record<PropertyKey, unknown>)[key] } return current as PathValue<T, P> } type Model = { user: { address: { city: string }; active: boolean } } declare const model: Model const city = getPath(model, 'user', 'address', 'city')
The build compiles this and checks each result below.
How it works
- 01
type Paths<T> = T extends object
The mapped type emits one tuple for every reachable property path.
- 02
PathValue<T[K], Rest>
The resolver follows the head key and recurses through the remaining tuple.
What you get
typeof city→stringPathValue<Model, ['user', 'active']>
→boolean['user', 'address', 'city'] extends Paths<Model> ? true : false
→true
Where it goes wrong
Very deep schemas can hit the compiler recursion limit, and arrays expose far more keys than most path APIs intend. Add an explicit depth cap or special array handling for large production models.
Takeaway
Represent a path as a tuple when every segment changes the type of the next segment.
