Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.5k views
in Technique[技术] by (71.8m points)

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

I have a function that returns any value of an object by a list of keys. Example:

Given this interface

interface Test {
  test1: string;
  test2: {
    test2Nested: {
      something: string;
      somethingElse: string;
    };
  };
}

I have an object

const test: Test = {
  test1: "",
  test2: {
    test2Nested: {
      something: "",
      somethingElse: "",
    },
  },
}
function getByPath<ObjectType, ReturnType) (obj: ObjectType, ...keys: string[]): ReturnType {
   return keys.reduce(
      (result: any, key: string) => result[key],
      obj
    );
}

getByPath(test, 'test2', 'test2Nested') will return

{
   something: "",
   somethingElse: "",
}

The question is: how can I make this function type safe especially the return type, to only contain valid partial and possibly nested values? Is this even possible?

Please check this snippet for an example

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...