2

Lodash has the function get(), which extracts a value from a complex object:

const object = { 'a': [{ 'b': { 'c': 3 } }] };
 
const n: 3 = _.get(object, 'a[0].b.c');

Lodash types get as:

type PropertyName = string | number | symbol;
type PropertyPath = Many<PropertyName>;
get(object: any, path: PropertyPath, defaultValue?: any): any;

Which is, in my opinion, somewhat weak. Monocle-ts solves a similar problem by just listing the first five or so possibilities:

export interface LensFromPath<S> {
  <
    K1 extends keyof S,
    K2 extends keyof S[K1],
    K3 extends keyof S[K1][K2],
    K4 extends keyof S[K1][K2][K3],
    K5 extends keyof S[K1][K2][K3][K4]
  >(
    path: [K1, K2, K3, K4, K5]
  ): Lens<S, S[K1][K2][K3][K4][K5]>
  <K1 extends keyof S, K2 extends keyof S[K1], K3 extends keyof S[K1][K2], K4 extends keyof S[K1][K2][K3]>(
    path: [K1, K2, K3, K4]
  ): Lens<S, S[K1][K2][K3][K4]>
  <K1 extends keyof S, K2 extends keyof S[K1], K3 extends keyof S[K1][K2]>(path: [K1, K2, K3]): Lens<S, S[K1][K2][K3]>
  <K1 extends keyof S, K2 extends keyof S[K1]>(path: [K1, K2]): Lens<S, S[K1][K2]>
  <K1 extends keyof S>(path: [K1]): Lens<S, S[K1]>
}

Which is great as far as it goes, but it only goes so far. Is there not a cleaner way?

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • Please see [this](https://tsplay.dev/WYZnQw), more explanation you can find in my [article](https://catchts.com/deep-pick) or these answers: [one](https://stackoverflow.com/questions/67242871/declare-a-type-that-allows-all-parts-of-all-levels-of-another-type#answer-67247652), [two](https://stackoverflow.com/questions/68668055/eliminate-nevers-to-make-union-possible/68672512?noredirect=1#comment121362429_68672512), [three](https://stackoverflow.com/questions/69126879/typescript-deep-keyof-of-a-nested-object-with-related-type#answer-69129328) – captain-yossarian from Ukraine Sep 21 '22 at 14:31
  • And here: [four](https://stackoverflow.com/questions/69449511/get-typescript-to-infer-tuple-parameters-types/69450150#69450150) – captain-yossarian from Ukraine Sep 21 '22 at 14:32
  • you can check this https://github.com/WilliBobadilla/dot2json and the engine that I used for this project – Williams Bobadilla Sep 21 '22 at 18:28

2 Answers2

1

Building on your existing answer, we can use this extremely useful type to narrow the type of path without needing as const:

type Narrow<T> =
    | (T extends infer U ? U : never)
    | Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
    | ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });

declare function get<S, P>(object: S, path: Narrow<P>): TypeAt<S, P>;

Then when you call it, P will be the inferred literal tuple:

const n: number = get(object, ['a', 0, 'b', 'c']) // get<{ ... }, ['a', 0, 'b', 'c']>(...);

Playground


Going two steps further, we can also validate the keys (with somewhat helpful error messages):

type ValidPath<P, O> = P extends [infer K, ...infer Rest] ? K extends keyof O ? [K, ...ValidPath<Rest, O[K]>] : [{ error: `Key '${K & string}' doesn't exist`, on: O }] : P;

declare function get<S,P>(object: S, path: ValidPath<Narrow<P>, S>): TypeAt<S, P>;

So when you make a mistake, for example, using "0" instead of 0:

const n: number = get(object, ['a', "0", 'b', 'c'])

You will get a friendly reminder that it doesn't exist:

Type 'string' is not assignable to type '{ error: "Key '0' doesn't exist"; on: { b: { c: number; }; }[]; }'.(2322)

Playground

kelsny
  • 23,009
  • 3
  • 19
  • 48
0

I found this imperfect solution:

type TypeAt<S, P> = P extends readonly [any, ...any] ? 
       (P extends readonly [(infer K extends keyof S), ...(infer Rest)] ? 
          TypeAt<S[K], Rest> : never) : S;

declare function get<S,P>(object: S, path: P): TypeAt<S, P>;

const object = { 'a': [{ 'b': { 'c': 3 } }] };
const n: number = get(object, ['a', 0, 'b', 'c'] as const)
 

The imperfection, to my mind, is that as const. Is there some way to cue the compiler in to not widen a literal object?

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • Yep, that'll look like [this](https://tsplay.dev/WvAeQm). By the way, if you are still interested in a solution that takes a string like your original example, let us know. – kelsny Sep 21 '22 at 14:53
  • @caTS — that is a much more thorough solution. If you post it as an answer I can accept it (since I am actually going to be using it at work). Ditto with the string — for my actual use, an array is better than a string but apparently, most people want a string. – Michael Lorton Sep 21 '22 at 17:40