特定属性的Deep /递归Required <T>

时间:2019-09-07 15:52:47

标签: typescript

给出一个这样的类:

class Example {
    always: number;
    example?: number;

    a?: {
        b?: {
            c?: number;
        }
    };

    one?: {
        two?: {
            three?: number;
            four?: number;
        }
    };
}

是否可以例如将a.b.cone.two.three标记为非可选(必需)属性,而无需更改example,也可以不更改one.two.four

我想知道ts-essentials中是否有MarkRequired的递归版本。

用例:

我们有一个类似ReST的API,该API返回总是定义一些属性的数据,而其他属性是可选的,并且由客户端显式请求(使用?with=a,b,c.d.e这样的查询字符串)。我们希望能够将请求的属性和嵌套的属性标记为不包含undefined,以避免不必要的undefined检查。

这样可能吗?

3 个答案:

答案 0 :(得分:3)

我用模板文字类型增加了 ford64 的答案,以允许使用点分隔的字符串指定路径,这在语法上看起来比键数组更熟悉。它不是 100% 相同的,因为您无法表达包含 . 的键;方括号不起作用 ([]);并且您可以以javascript不允许的方式表达键,例如a.b-c.d for obj.a[b-c].d;但这些都非常小,如果有人真的想要的话,增加这种类型以至少支持括号情况会非常简单。

Here's a playground link demonstrating it! 我稍微编辑了类型的名称,简化了一些类型并去掉了 any 的不必要使用,尽管我仍然不明白 ShiftUnion 类型是如何从上一个答案可以解决问题,所以我放弃了。

基本上,您采用了 ford04 的答案,只需将需要的路径包装在 PathToStringArray 类型中。

type PathToStringArray<T extends string> = T extends `${infer Head}.${infer Tail}` ? [...PathToStringArray<Head>, ...PathToStringArray<Tail>] : [T]

// ford04's answer, then

type DeepRequiredWithPathsSyntax<T, P extends string> = DeepRequired<T, PathsToStringArray<P>>

结果是您可以使用点分隔语法来创建这些路径,而不是冗长的数组语法,如下所示:

type Foo = { a?: 2, b?: { c?: 3, d: 4 } }
type A = RequireKeysDeep<Foo, "a">; // {a: 2, b?: { c?: 3, d: 4 } }
type B = RequireKeysDeep<Foo, "b">; // {a?: 2, b: { c?: 3, d: 4 } }
type BC = RequireKeysDeep<Foo, "b.c">; // {a?: 2, b: { c: 3, d: 4 } }
type ABC = RequireKeysDeep<Foo, "a" | "b.c">; // {a: 2, b: { c: 3, d: 4 } }

测试在操场链接中。

答案 1 :(得分:2)

这就是我想出的创建递归DeepRequired类型的方法。

输入

两个通用类型参数:

  1. T为基本类型Example
  2. P用于表示元组的联合类型,它表示我们的“必需的对象属性路径” ["a", "b", "c"] | ["one", "two", "three"](类似于通过get的lodash对象路径)

示例流程

  1. 在顶级P[0]中获取所有必需的属性:"a" | "one"
  2. 创建必需和非必需对象属性的交集类型/并置

我们包含了Example中的所有属性,并另外创建了一个mapped type来删除?undefined的值,这些值将被更改为必需的每个可选属性。我们可以使用内置类型RequiredNonNullable来做到这一点。

type DeepRequired<T, P extends string[]> = T extends object
  ? (Omit<T, Extract<keyof T, P[0]>> &
      Required<
        {
          [K in Extract<keyof T, P[0]>]: NonNullable<...> // more shortly 
        }
      >)
  : T;
  1. 对于子属性,该类型必须以某种方式递归。这意味着,我们还必须找到一种方法,以将类型从元组T“转移”到迭代地获取路径中的下一个必需的子属性。为此,我们创建一个辅助元组类型Shift(稍后将在实现中进行更多介绍)。
type T = Shift<["a", "b", "c"]> 
       = ["b", "c"]
  1. 具有挑战性的是,我们希望传递一个元组的联合(又称许多必需的路径),而不仅仅是一个。我们可以为此使用distributive conditional types并使用另一个帮助程序ShiftUnion来在包含Shift的条件类型上分配元组的并集:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]> 
       = ["b", "c"] | ["two", "three"]
  1. 然后,我们只需选择第一个索引即可获得下一个子路径的所有必需属性:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>[0] 
       = "b" | "two"

实施

主要类型DeepRequired

type DeepRequired<T, P extends string[]> = T extends object
  ? (Omit<T, Extract<keyof T, P[0]>> &
      Required<
        {
          [K in Extract<keyof T, P[0]>]: NonNullable<
            DeepRequired<T[K], ShiftUnion<P>>
          >
        }
      >)
  : T;

元组帮助程序类型Shift / ShiftUnion

我们可以在generic rest parameters in function typestype inference in conditional types的帮助下推断元组类型,该元组类型将移动一个元素。

// Analogues to array.prototype.shift
export type Shift<T extends any[]> = ((...t: T) => any) extends ((
  first: any,
  ...rest: infer Rest
) => any)
  ? Rest
  : never;

// use a distributed conditional type here
type ShiftUnion<T> = T extends any[] ? Shift<T> : never;

测试

type DeepRequiredExample = DeepRequired<
  Example,
  ["a", "b", "c"] | ["one", "two", "three"]
>;

declare const ex: DeepRequiredExample;

ex.a.b.c; // (property) c: number
ex.one.two.three; // (property) three: number
ex.one.two.four; // (property) four?: number | undefined
ex.always // always: number
ex.example // example?: number | undefined

Playground


一些波兰语(更新)

仍然存在一些小错误:如果我们还在two下添加属性a,例如a?: { two?: number; ... };,它也被标记为必填项,尽管在示例中路径P中没有被["a", "b", "c"] | ["one", "two", "three"]占用。我们可以通过扩展ShiftUnion类型来轻松解决此问题:

type ShiftUnion<P extends PropertyKey, T extends any[]> = T extends any[]
  ? T[0] extends P ? Shift<T> : never
  : never;

示例:

// for property "a", give me all required subproperties
// now omits "two" and "three"
type T = ShiftUnion<"a", ["a", "b", "c"] | ["one", "two", "three"]>;
       = ["b", "c"]

此实现不包括位于不同“对象路径”中的同名属性,例如two。因此,two下的a不再标记为必需。

Playground

可能的扩展名

  • 为方便起见,为方便起见,传递字符串而不是元组路径。
  • 当前实现适用于一些需要标记为必需的对象路径;如果要从一个对象中选择多个嵌套子属性,则可以扩展该解决方案以接收对象文字类型而不是元组。

希望,有帮助!随意将其用作进一步实验的基础。

答案 2 :(得分:0)

这对我很有效:

//Custom utility type:
export type DeepRequired<T> = {
  [K in keyof T]: Required<DeepRequired<T[K]>>
}

//Usage:
export type MyTypeDeepRequired = DeepRequired<MyType>

自定义实用程序类型采用任何类型,并迭代地将其键设置为所需并递归调用更深层次的结构,并执行相同的操作。结果是一个新类型,其中深度嵌套类型的所有参数都设置为 required。

我从这篇文章中得到了这个想法,他使深度嵌套的类型可以为空:

type DeepNullable<T> = {
  [K in keyof T]: DeepNullable<T[K]> | null;
};

https://typeofnan.dev/making-every-object-property-nullable-in-typescript/

因此,此方法可用于更改深度嵌套属性的任意属性。