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?
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 ¯\_(ツ)_/¯