特定属性上的深度/递归 Required<T>

gle*_*-84 5 typescript

给定一个这样的类:

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

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

    one?: {
        two?: {
            three?: number;
            four?: number;
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

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

我想知道是否有一些递归版本的MarkRequired from ts-essentials.

用例:

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

这样的事情可能吗?

Ema*_*röm 17

这对我来说效果很好:

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

//Usage:
export type MyTypeDeepRequired = DeepRequired<MyType>
Run Code Online (Sandbox Code Playgroud)

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

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

type DeepNullable<T> = {
  [K in keyof T]: DeepNullable<T[K]> | null;
};
Run Code Online (Sandbox Code Playgroud)

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

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

  • 对于发现此内容的任何人:`type DeepRequired&lt;T&gt; = { [K in keyof T]: DeepRequired&lt;T[K]&gt;} &amp;Required&lt;T&gt;` 可能是最好使用的东西。 (7认同)
  • @Zarjio 这是为什么? (2认同)

for*_*d04 7

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

输入

两个泛型类型参数:

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

示例流程

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

我们包括所有属性,Example并另外创建一个要删除的映射类型?以及undefined要更改为必需的每个可选属性的值。我们可以通过使用内置类型Required和NonNullable.

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;
Run Code Online (Sandbox Code Playgroud)
  1. 对于子属性,该类型必须以某种方式递归。这意味着,我们还必须找到一种方法来从元组“移动”类型T以迭代地获取路径中的下一个必需的子属性。为此,我们创建了一个辅助元组类型Shift(稍后将详细介绍实现)。
type T = Shift<["a", "b", "c"]> 
       = ["b", "c"]
Run Code Online (Sandbox Code Playgroud)
  1. 具有挑战性的事情是,我们想要传入一组元组(也就是许多必需的路径),而不仅仅是一个。我们可以为此使用分布条件类型,并使用另一个ShiftUnion能够在包含Shift以下条件的条件类型上分布元组联合的助手:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]> 
       = ["b", "c"] | ["two", "three"]
Run Code Online (Sandbox Code Playgroud)
  1. 然后我们可以通过简单地选择第一个索引来获取下一个子路径的所有必需属性:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>[0] 
       = "b" | "two"
Run Code Online (Sandbox Code Playgroud)

执行

主要类型 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;
Run Code Online (Sandbox Code Playgroud)

元组辅助类型Shift/ShiftUnion

借助函数类型中的通用其余参数和条件类型中的类型推断,我们可以推断出元组类型,即移动了一个元素。

// 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;
Run Code Online (Sandbox Code Playgroud)

测试

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
Run Code Online (Sandbox Code Playgroud)

操场


一些波兰语(更新)

那里仍留有一些小的不准确:如果我们增加财产two也下a,例如a?: { two?: number; ... };,它也被标记为需要的,尽管我们的道路不会beeingP用["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;
Run Code Online (Sandbox Code Playgroud)

例子:

// 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"]
Run Code Online (Sandbox Code Playgroud)

此实现不包括two在不同“对象路径”中的同名属性,例如。所以twoundera不再标记为 required 了。

操场

可能的扩展

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

希望,这有帮助!随意使用它作为您进一步实验的基础。


osd*_*iab 6

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

这是演示它的游乐场链接!我稍微编辑了类型的名称,简化了一些类型并删除了不必要的使用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>>
Run Code Online (Sandbox Code Playgroud)

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

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 } }
Run Code Online (Sandbox Code Playgroud)

测试在 Playground 链接中进行。