Declare a type that allows all parts of all levels of another type

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)

Leave a Comment