Given is a tuple of some keys like ["a", "b", "c"]
and a nested object with those keys as properties {a: {b: {c: number}}}
. How do you recursively use the members of the tuple as index in typescript?
A implementation without proper typing:
function recursivePluck(ob: any, tuple: any[]): any {
for (let index of tuple) {
ob = ob[index]
}
return ob
}
How do you type the above code?
I have tried the following
type RecursivePluck<
Tuple extends string[],
Ob extends {[key in string]: any},
TupleWithoutFirst extends SliceStartQuantity<Tuple, 1> = SliceStartQuantity<Tuple, 1>
>
= TupleWithoutFirst extends [] ? Ob[Tuple[0]] : RecursivePluck<TupleWithoutFirst, Ob[Tuple[0]]>
But this errors Type alias 'RecursivePluck' circularly references itself.
Note that
SliceStartQuantity
is fromtypescript-tuple
(npm)
Here the solution, it covers type safety of the argument and of the return type:
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]
// this was done to avoid infinite processing the type by TS
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 will give us 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]
// checks if type is working
type Test = RecursivePath<{a: {b: {c: string}, d: string}}>
type Test2 = RecursivePath<{a: {b: {c: {e: string}}, d: string}}>
// this type will give as value type at 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]
// checks if type is working
type Test3 = RecursivePathValue<{a: {b: {c: string}, d: string}},['a', 'b']>
type Test4 = RecursivePathValue<{a: {b: {c: {e: string}}, d: string}}, ['a','d']>
// finnaly the function
function recursivePluck<Obj extends object, Path extends RecursivePath<Obj>>(ob: Obj, tuple: Path): RecursivePathValue<Obj, Path> {
// inside I just fallback to any
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