Patrones
¿Cómo leo `user.address.city` mediante una ruta reutilizable sin perder el tipo del resultado?
Genera recursivamente las tuplas de claves válidas y consume una clave cada vez para calcular el valor final.
La receta
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')
La compilación compila esto y comprueba cada resultado de abajo.
Cómo funciona
- 01
type Paths<T> = T extends object
El tipo mapeado emite una tupla por cada ruta de propiedad alcanzable.
- 02
PathValue<T[K], Rest>
El resolutor sigue la primera clave y recorre recursivamente el resto de la tupla.
Lo que obtienes
typeof city→stringPathValue<Model, ['user', 'active']>
→boolean['user', 'address', 'city'] extends Paths<Model> ? true : false
→true
Dónde falla
Los esquemas muy profundos pueden alcanzar el límite de recursión y los arrays exponen más claves de las que suele querer una API de rutas. Añade un límite de profundidad o tratamiento especial de arrays en modelos grandes.
Conclusión
Representa una ruta como tupla cuando cada segmento cambia el tipo del siguiente.
