Provided is a comprehensive solution that addresses the type safety of both argument and return types:
type Unshift<A, T extends Array<any>>
= ((a: A, ...b: T) => any) extends ((...result: infer Result) => any) ? Result : never;
type Shift<T extends Array<any>>
= ((...a: T) => any) extends ((a: any, ...result: infer Result) => any) ? Result : never;
type Revert
<T extends Array<any>
, Result extends Array<any> = []
, First extends T[keyof T] = T[0]
, Rest extends Array<any> = Shift<T>> = {
[K in keyof T]: Rest['length'] extends 0 ? Unshift<First, Result> : Revert<Rest, Unshift<First, Result>>
}[0]
// To prevent infinite processing by TypeScript
type Level = 0 | 1 | 2 | 3 | 4 | 5
type NextLevel<X extends Level> =
X extends 0 ? 1 :
X extends 1 ? 2 :
X extends 2 ? 3 :
X extends 3 ? 4 :
X extends 4 ? 5 :
never
// This type gives us the possible path type for the object
type RecursivePath<Obj extends object, Result extends any[] = [], Lv extends Level = 0> = {
[K in keyof Obj]:
Lv extends never ? Result :
Obj[K] extends object ? (Result['length'] extends 0 ? never : Revert<Result>) | RecursivePath<Obj[K], Unshift<K, Result>, NextLevel<Lv>> :
Revert<Result> | Revert<Unshift<K,Result>>
}[keyof Obj]
// Validate if the type is functioning correctly
type Test = RecursivePath<{a: {b: {c: string}, d: string}}>
type Test2 = RecursivePath<{a: {b: {c: {e: string}}, d: string}}>
// Retrieve the value type at a given path
type RecursivePathValue<Obj, Path extends any> =
{
[K in keyof Path]:
Path extends any[] ?
Path[K] extends keyof Obj ?
Path['length'] extends 1 ? Obj[Path[K]] : RecursivePathValue<Obj[Path[K]], Shift<Path>>
: never
: never
}[number]
// Validate if the type is functioning correctly
type Test3 = RecursivePathValue<{a: {b: {c: string}, d: string}},['a', 'b']>
type Test4 = RecursivePathValue<{a: {b: {c: {e: string}}, d: string}}, ['a','d']>
// The main function definition
function recursivePluck<Obj extends object, Path extends RecursivePath<Obj>>(ob: Obj, tuple: Path): RecursivePathValue<Obj, Path> {
// Use 'any' as a fallback inside
let result: any = ob;
for (let index of tuple as any[]) {
result = result[index]
}
return result;
}
const a = recursivePluck({a: {b: {c: {d: 'value'}}}}, ['a','b']) // OK
const b = recursivePluck({a: {b: {c: {d: 'value'}}}}, ['a','e']) // Error
const c = recursivePluck({a: {b: {c: {d: 'value'}}}}, ['a','b','e']) // Error
const d = recursivePluck({a: {b: {c: {d: 'value'}}}}, ['a','b','c']) // OK
const e = recursivePluck({a: {b: {c: {d: 'value'}}}}, ['a','b','c', 'd']) // OK
View Playground