打字稿:使用元组作为索引类型

Typescript: Using a tuple as index type

给出的是一些键的元组,如 ["a", "b", "c"] 和一个以这些键作为属性的嵌套对象 {a: {b: {c: number}}}。你如何递归地使用元组的成员作为打字稿中的索引?

没有正确输入的实现:

function recursivePluck(ob: any, tuple: any[]): any {
  for (let index of tuple) {
    ob = ob[index]
  }
  return ob
}

如何输入以上代码?

我试过以下方法

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]]>

但是这个错误Type alias 'RecursivePluck' circularly references itself.

Note that SliceStartQuantity is from typescript-tuple (npm)

这里是解决方案,它涵盖了参数和 return 类型的类型安全:

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

The Playground