Let's do it in several steps.
// Simple union type for primitives
type Primitives = string | number | symbol;
Our type for keys
should allow props in strict order and it should be an array (because of rest
operator).
I think the best here is to create a union type of all possible arguments.
Let's do it.
type NestedKeys<T, Cache extends Array<Primitives> = []> = T extends Primitives ? Cache : {
[P in keyof T]: [...Cache, P] | NestedKeys<T[P], [...Cache, P]>
}[keyof T]
// ["test1"] | ["test2"] | ["test2", "test2Nested"] | ["test2", "test2Nested", "something"] | ["test2", "test2Nested", "somethingElse"] | ["test2", "test2Nested", "test3Nestend"] .....
Now, we should write a type for our reducer logic.
type Elem = string;
type Predicate<Result extends Record<string, any>, T extends Elem> = T extends keyof Result ? Result[T] : never
type Reducer<
Keys extends ReadonlyArray<Elem>,
Accumulator extends Record<string, any> = {}
> = Keys extends []
? Accumulator
: Keys extends [infer H]
? H extends Elem
? Predicate<Accumulator, H>
: never
: Keys extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? Reducer<Tail, Predicate<Accumulator, H>>
: never
: never
: never;
This type is doing almost exactly what You did in reducer. Why almost? Because it is recursive type.
I gave same names for variables, so it will be much easier to understand what happens here.
More examples You can find here, in my blog.
After we have created all our types, we can implement the function with tests:
const getByPath = <Obj, Keys extends NestedKeys<Obj> & string[]>(obj: Obj, ...keys: Keys): Reducer<Keys, Obj> =>
keys.reduce((acc, elem) => acc[elem], obj as any)
getByPath(test, 'test1') // ok
getByPath(test, 'test1', 'test2Nested') // expected error
getByPath(test, 'test2') // ok
const result = getByPath(test, 'test2', 'test2Nested') // ok -> { something: string; somethingElse: string; test3Nestend: { end: string; }; }
const result3 = getByPath(test, 'test2', 'test2Nested', 'test3Nestend') // ok -> {end: stirng}
getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'test2Nested') // expeted error
const result2=getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'end') // ok -> string
getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'end', 'test2') // expected error
Playground
More exaplanation You can find in my blog
DOT NOTATION
type Foo = {
user: {
description: {
name: string;
surname: string;
}
}
}
declare var foo: Foo;
type Primitives = string | number | symbol;
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, any> =>
Object.prototype.hasOwnProperty.call(obj, prop);
type KeysUnion<T, Cache extends string = ''> =
T extends Primitives ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type O = KeysUnion<Foo>
type ValuesUnion<T, Cache = T> =
T extends Primitives ? T : Values<{
[P in keyof T]:
| Cache | T[P]
| ValuesUnion<T[P], Cache | T[P]>
}>
declare function deepPickFinal<Obj, Keys extends KeysUnion<Obj>>
(obj: ValuesUnion<Obj>, keys: Keys): Reducer<Keys, Obj>
/**
* Ok
*/
const result = deepPickFinal(foo, 'user') // ok
const result2 = deepPickFinal(foo, 'user.description') // ok
const result3 = deepPickFinal(foo, 'user.description.name') // ok
const result4 = deepPickFinal(foo, 'user.description.surname') // ok
/**
* Expected errors
*/
const result5 = deepPickFinal(foo, 'surname')
const result6 = deepPickFinal(foo, 'description')
const result7 = deepPickFinal(foo)