这就是我想出的创建递归DeepRequired 类型的方法。
输入
两个泛型类型参数:
-
T 用于基本类型 Example
-
P 用于联合类型的元组,表示我们的“必需的对象属性路径”["a", "b", "c"] | ["one", "two", "three"](类似于通过get 的lodash 对象路径)
示例流程
- 获取顶层
P[0]中所有必需的属性:"a" | "one"
- 创建必需和非必需对象属性的交集类型/串联
我们包含来自Example 的所有属性,并另外创建一个mapped type 以删除? 和每个要更改为必需的可选属性的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;
- 子属性的类型必须以某种方式递归。这意味着,我们还必须找到一种方法将类型从元组
T “转移”,以迭代地获取路径中的下一个所需子属性。为此,我们创建了一个辅助元组类型Shift(稍后将详细介绍实现)。
type T = Shift<["a", "b", "c"]>
= ["b", "c"]
- 具有挑战性的事情是,我们想要传递一个元组的联合(也就是许多必需的路径),而不仅仅是一个。为此,我们可以使用distributive conditional types 并使用另一个帮助程序
ShiftUnion 能够在包含Shift 的条件类型上分配元组的联合:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>
= ["b", "c"] | ["two", "three"]
- 然后我们可以通过简单地选择第一个索引来获取下一个子路径所需的所有属性:
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 types 和type 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
可能的扩展
- 为方便起见,对于单个必需属性,传入字符串而不是元组路径。
- 当前实现适用于需要标记的少数对象路径;如果要从一个对象中选择多个嵌套子属性,则可以扩展解决方案以接收对象字面量类型而不是元组。
希望,这会有所帮助!随意将其用作您进一步实验的基础。