Annotated solution
Published August 29, 2026The solution
type Paths<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? K | `${K}.${Paths<T[K]> & string}` : K }[keyof T & string] : never
The common wrong answer
type Paths<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}.${Paths<T[K]> & string}` : K }[keyof T & string] : never
This yields only the leaves: `"db.host" | "db.port" | "debug"`. The branch for an object-valued key returns the prefixed descendants but forgets that the key itself is a valid path too. The `K |` is doing the entire job of making intermediate paths appear.
Line by line
}[keyof T & string]The mapped type is scaffolding, never the answer. Indexing it by all its own keys reads out every value at once and unions them — the standard way to turn a per-key computation into a union.
Paths<T[K]> & stringThe intersection is not decoration. A template literal will only interpolate something the compiler already knows is string-like, and a deferred recursive call is not — `& string` is the promise that lets it through.
keyof T & stringObject keys can be symbols, which cannot appear in a path string. Intersecting with `string` filters them out at both the mapping and the indexing step, and the two must agree or the result collapses.
Takeaway
A mapped type indexed by its own keys is the type-level equivalent of `Object.values().flat()`. Combined with recursion it walks arbitrarily deep structures — and this exact shape is how typed form libraries and i18n key checkers are built.
