【问题标题】:Deep/recursive Required<T> on specific properties特定属性的深度/递归Required<T>
【发布时间】:2021-06-04 16:20:58
【问题描述】:

给定这样的课程:

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

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

用例:

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

这样的事情可能吗?

【问题讨论】:

  • Typescript 无法将诸如 "a.b" (只是一个字符串)之类的东西“解析”为嵌套属性,因此,如果这是您正在寻找的内容的一部分(我认为是),那么答案是否定的。如果您对语法更灵活,那就另当别论了。
  • 如果您对 MarkRequired&lt;Example, { a: { b: { c: any } }; one: { two: { three: any } } }&gt; 这样的语法没问题,请告诉我,我会发布答案。
  • 这不正是non-null assertions 的用途吗?
  • @Patrick Roberts 不过,你把这个负担放在了 API 的使用者身上。我认为这里的 OP 想要返回一个已经包含这个的正确类型的响应。
  • @Ingo Bürk 不,我很确定 TS 无法理解 a.b 语法,问题更多是关于将 a 标记为必需,然后在它下面也有属性按要求标记。谢谢。

标签: typescript


【解决方案1】:

这就是我想出的创建递归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


一些润色(更新)

还有一些小错误:如果我们在a 下添加属性two,例如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。所以a下的two不再被标记为必需。

Playground

可能的扩展

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

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

【讨论】:

  • 太棒了,谢谢。当它只是一个属性时,比如example,如果它不必是一个元组(比如"example" | ["one", "two"])可能会很酷,但这只是一件小事,如果它使事情变得复杂,就不值得实现了。
  • @glen-84 谢谢!那个单一的字符串类型可能是一个方便的扩展,你是对的。顺便说一句:我对ShiftUnion 类型添加了一个小修复,请查看更新后的答案。
  • 现在使用模板文字类型,我敢打赌这可以重写为使用像“one.two.three”这样的字符串语法:)
  • 是的,这种类型会将"a.b.c.d" 之类的字符串类型转换为["a", "b", "c", "d"],因此您可以使用上面的答案和以下更熟悉的语法:) type PathToStringArray&lt;T extends string&gt; = T extends `${infer Head}.${infer Tail}` ? [...PathToStringArray&lt;Head&gt;, ...PathToStringArray&lt;Tail&gt;] : [T]
  • @osdiab T extends any 的目的是在extends 子句上分配每个 联合成员,以便ShiftUnion&lt;"a", ["a", "b"] | ["a", "c"]&gt; 变为["b"] | ["c"](参见TS docs )。
【解决方案2】:

我用模板文字类型扩充了 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 } }

测试在 Playground 链接中。

【讨论】:

  • 不错!当我阅读模板文字类型时,我想知道这是否可能。
  • 我不认为有办法让这个与数组一起工作?它确实适用于特定索引,例如 roles.0.id,但如果我们可以使用 roles.*.idroles.[number].id 之类的东西,那就太棒了。
  • 我正在寻找的另一件事是 Pick 属性,而不是根据需要标记它们。 f.e. Pick&lt;Foo, "a" | "b.c"&gt;Pick&lt;Foo, "a" | "b"&gt;(除了 a 之外,b 的所有属性都被选中)。
  • 前者可能是可能的——类型会变得更复杂,但基本上当你看到 * 你会用数组中的any 替换它。它会破坏其中带有 * 的键,但谁使用键名中的那些?后者非常简单 - 使用上面的 PathToStringArray 助手,那么我相信这应该可以工作:type PickDeep&lt;T, P extends string&gt; = Pick&lt;T, PathToStringArray&lt;P&gt;[number]&gt;
  • 哦,没关系,taht 不起作用,但对于 PickDeep,但它绝对可能以与 DeepRequired 相同的方式起作用,只是没有花时间解决它。
【解决方案3】:

这对我很有效:

//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/

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

【讨论】:

  • 我认为您可能错过了问题的“特定属性”部分。 ?顺便说一句,您可以使用[P in keyof T]-?: DeepRequired&lt;T[P]&gt;;缩短DeepRequired&lt;T&gt;
  • 对于发现此内容的任何人:type DeepRequired&lt;T&gt; = { [K in keyof T]: DeepRequired&lt;T[K]&gt;} &amp; Required&lt;T&gt; 可能是最好的选择。
猜你喜欢
  • 2017-07-03
  • 2018-11-27
  • 2020-05-26
  • 1970-01-01
  • 2022-12-19
  • 2017-02-20
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
相关资源
最近更新 更多