typescriptrecursiontuplespluckindex-signature

Typescript: Using a tuple as index type


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 from typescript-tuple (npm)


Solution

  • 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
    

    The Playground