Typescript JSON Path Type

Viewed 343

I'm trying to build a few types so that I can define a generic function that

  • takes an object and a JSON-path
    (arrays can be ignored for the moment)
  • automatically deduce the correct return type
  • IDE-AutoComplete hints for all possible paths

I've successfully managed to accomplish the first to bullets, however, I cannot get any autocomplete hints.

I've tried to build a working type that creates a union of all valid-paths. But this is where I got stuck.

Here's what I got so far

// helper types
type Length<T> = T extends { length: infer L } ? L : never;

type PopFront<T extends unknown[]> = T extends [infer U, ...infer R] ? U : never;
type Shift<T extends unknown[]> = T extends [infer U, ...infer R] ? R : never;

type Filter<T extends unknown[], U> = T extends [] ? [] : T extends [infer F, ...infer R] ? F extends U ? Filter<R, T> : [F, ...Filter<R, U>] : never
type TupleIncludes<T extends unknown[], U> = Length<Filter<T, U>> extends Length<T> ? false : true
type StringIncludes<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? true : false; 

type Includes<T extends unknown[]|string, U> = T extends unknown[] ? TupleIncludes<T, U> : T extends string ? U extends string ? StringIncludes<T, U> : never : never
type Join<T extends unknown[], D extends string> = PopFront<T> extends string ? Length<T> extends 1 ? `${PopFront<T>}` : Shift<T> extends string[] ? `${PopFront<T>}${D}${Join<Shift<T>, D>}` : never : never; 

type Split<S extends string, D extends string> =
    string extends S ? string[] :
        S extends '' ? [] :
            S extends `${infer T}${D}${infer U}` ?  [T, ...Split<U, D>] : [S];

// return type deduction from path
type NestedType<T, P extends string> = (
  Includes<P, '.'> extends true
    ? PopFront<Split<P, '.'>> extends keyof T
      ? NestedType<T[PopFront<Split<P, '.'>>], Join<Shift<Split<P, '.'>>, '.'>>
      : never
    : P extends keyof T ? T[P] : never
);

// Demo

const dictionary = {
  someProp: 123,
  nested: {
    moreProps: 333,
    deeper: {
      evenDeeper: {
        deepest: 'str'
      }
    },
    alsoDeeper: {
      randomProp: {
        anotherProp: 'yay'
      }
    }
  }
} as const;
type MyDict = typeof dictionary;

type Test1 = NestedType<MyDict, 'nested.alsoDeeper.randomProp.anotherProp'>; // = yay

const Fn = <T, P extends string>(dict: T, path: P): NestedType<T, P> => {
  // skip impl.
  return undefined as any;
};

const testFromFn = Fn(dictionary, 'nested.moreProps'); // = 333

The code above is all fine and well, now to the type I've tried to build, which should give me all valid paths:

// If you look at type "Debug" below, this surprisingly works.
// ...apart from the `Type instantiation is excessively deep and possibly infinite.`-error
type ValidPaths<T> = keyof T extends never ? never : ({
     [K in keyof T]: T[K] extends never ? never : T[K] extends Record<string|number|symbol, unknown>
       ? K extends string ? `${K}.${ValidPaths<T[K]>}` | K : never
       : K
   })[keyof T];
type Debug = ValidPaths<MyDict>;

// So I also tried with Tuples.
type ValidPathTuples<T> = keyof T extends never ? never : ({
     [K in keyof T]: T[K] extends never ? never : T[K] extends Record<string|number|symbol, unknown>
       ? [K, ...ValidPathTuples<T[K]>] | [K]
       : [K]
   })[keyof T];
// Which seems fine, too!
type DebugTuples = ValidPathTuples<MyDict>;
// But now, I cannot join them to path-strings
type DebugTuples1 = Join<ValidPathTuples<MyDict>, '.'>;

What exactly am I missing here, or am I on the wrong path?
Why exactly does the tuple version work, but the one using string-literal-templates doesn't?

Playground

Edit:

I just figured something out.

I changed my Join-type to this

type Join<T extends unknown[], D extends string> = T extends string[]
  ? PopFront<T> extends string ? Length<T> extends 1 ? `${PopFront<T>}` : `${PopFront<T>}${D}${Join<Shift<T>, D>}` : never
  : never;

Now the Join<ValidPathTuples, '.'> produces the correct result. The only thing done was to remove Shift<T> extends string[] ? trueExpr : falseExpr; and just use the removed trueExpr

I don't really understand what difference that made ¯\_(ツ)_/¯

0 Answers
Related