给出一个这样的类:
class Example {
always: number;
example?: number;
a?: {
b?: {
c?: number;
}
};
one?: {
two?: {
three?: number;
four?: number;
}
};
}
是否可以例如将a.b.c
和one.two.three
标记为非可选(必需)属性,而无需更改example
,也可以不更改one.two.four
?
我想知道ts-essentials
中是否有MarkRequired的递归版本。
用例:
我们有一个类似ReST的API,该API返回总是定义一些属性的数据,而其他属性是可选的,并且由客户端显式请求(使用?with=a,b,c.d.e
这样的查询字符串)。我们希望能够将请求的属性和嵌套的属性标记为不包含undefined
,以避免不必要的undefined
检查。
这样可能吗?
答案 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
类型的方法。
两个通用类型参数:
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"]
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
仍然存在一些小错误:如果我们还在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
不再标记为必需。
希望,有帮助!随意将其用作进一步实验的基础。
答案 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/
因此,此方法可用于更改深度嵌套属性的任意属性。