声明一个类型,允许其他类型的所有级别的所有部分 [英] Declare a type that allows all parts of all levels of another type

查看:20
本文介绍了声明一个类型,允许其他类型的所有级别的所有部分的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

给定这个接口

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: "",
}

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

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?

请检查此代码段一个例子

推荐答案

让我们分几个步骤来做.

Let's do it in several steps.

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

我们的 keys 类型应该允许 props 有严格的顺序并且它应该是一个数组(因为 rest 操作符).我认为这里最好的是创建所有可能参数的联合类型.

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.

让我们去做吧.

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 逻辑编写一个类型.

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;

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

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

游乐场

更多解释你可以在我的博客中找到

点符号


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)

这篇关于声明一个类型,允许其他类型的所有级别的所有部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆