【问题标题】:Declare a type that allows all parts of all levels of another type声明一个类型,允许其他类型的所有级别的所有部分
【发布时间】:2021-07-18 10:10:50
【问题描述】:

我有一个函数,它通过键列表返回对象的任何值。 示例:

给定这个接口

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

我有一个对象

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') 将返回

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

问题是:我怎样才能使这个函数类型安全,尤其是返回类型,只包含有效的部分值和可能的嵌套值?这甚至可能吗?

请查看this snippet 示例

【问题讨论】:

  • 如果它有真正的标识符名称,这将是一个更好的例子。 Foo Bar 示例很少建立足够的上下文来提供足够的答案。

标签: typescript


【解决方案1】:

让我们分几个步骤来做。

// Simple union type for primitives
type Primitives = string | number | symbol;

keys 的类型应该允许按严格顺序排列的道具,并且它应该是一个数组(因为 rest 运算符)。 我认为这里最好的方法是创建一个包含所有可能参数的联合类型。

让我们开始吧。

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"] .....

现在,我们应该为我们的 reducer 逻辑编写一个类型。

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;

这种类型几乎和你在 reducer 中所做的完全一样。为什么差不多?因为它是递归类型。 我为变量提供了相同的名称,因此更容易理解这里发生的事情。 更多示例您可以在我的博客中找到here

创建完所有类型后,我们可以通过测试来实现函数:

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

点符号


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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-12-03
    • 2021-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多