Read nested values with a type-safe path
TypeScript
// Dotted paths are checked at compile time and resolve to the value type.
type Primitive = string | number | boolean | null | undefined;
type Paths<T> = T extends Primitive ? never : {
[K in keyof T & string]: T[K] extends Primitive ? K : K | `${K}.${Paths<T[K]>}`;
}[keyof T & string];
type PathValue<T, P extends string> = P extends `${infer Head}.${infer Tail}`
? Head extends keyof T ? PathValue<T[Head], Tail> : never
: P extends keyof T ? T[P] : never;
function getPath<T, P extends Paths<T> & string>(source: T, path: P): PathValue<T, P> {
let current: unknown = source;
for (const key of path.split('.')) {
if (current === null || typeof current !== 'object') {
throw new TypeError(`cannot read "${path}"`);
}
current = (current as Record<string, unknown>)[key];
}
return current as PathValue<T, P>;
}
const user = { name: 'Ada', address: { city: 'London', zip: 'N1' } };
console.log(getPath(user, 'address.city').toUpperCase()); // LONDON