Ree*_*mes 9 types typescript typescript-generics typescript-typings
我有一个案例,我想“合并”默认类型加入(即T | U或T & U)无法实现我想要的类型。
我想要做的是一个深度和智能的类型合并,它会在合并过程中自动将属性标记为可选,并执行 TypeScript 接口/类型的深度合并。
举个例子,假设我们有类型A和B。
type A = {
a: string;
b: number;
c: boolean;
d: {
a2: string;
b2: number;
};
e?: number;
};
type B = {
a: string;
b: boolean;
d: {
a2: string;
c2: boolean;
};
};
Run Code Online (Sandbox Code Playgroud)
我正在寻找一个Merge可以接受 2 种泛型类型的函数
type Merge<T, U> = ?????;
Run Code Online (Sandbox Code Playgroud)
然后,如果在类型Aand 上使用B,输出将如下所示
type AB = {
a: string;
b: number | boolean;
c?: boolean;
d: {
a2: string;
b2?: number;
c2?: boolean;
};
e?: number;
};
Run Code Online (Sandbox Code Playgroud)
如图所示,该Merge类型将执行以下逻辑:
TandU并且是相同的类型,则将其标记为需要并设置为两个T/ 中的类型U(就像发生在 property 中的那样a)。T并且U是不同的类型,则将其标记为需要并设置为联合类型,如果它是原始类型(就像发生在 property 中的那样b),或者如果它是一个对象,则执行递归合并(就像发生在 with财产d)。c以及b2and发生的情况c2)。e)假设您可以使用递归条件类型,尽管我认识到它们尚未得到官方支持,不应在生产中使用。我可以做一个展开的版本相似,jcalz @的解决方案在这里生产用例。
Ger*_*it0 19
TLDR:魔法!试试游乐场
所以,这是一个棘手的问题。不是因为合并要求,而是因为边缘情况。获得低悬的果实花了 <20 分钟。确保它在任何地方都能工作需要几个小时……而且长度增加了两倍。工会太难了!
什么是可选属性?in{ a: 1 | undefined, b?: 1 }是a可选属性吗?有些人说是的。其他没有。就个人而言,我只包括b在可选列表中。
你如何处理工会?的输出是Merge<{}, { a: 1} | { b: 2 }>什么?我认为最有意义的类型是{ a?: 1 } | { b?: 2 }. 怎么样Merge<string, { a: 1 }>?如果你根本不关心工会,这很容易……如果你关心,那么你必须考虑所有这些。(我在括号中选择的)
Merge<never, never>( never)Merge<never, { a: 1 }>( { a?: 1 })Merge<string, { a: 1 }>( string | { a?: 1 })Merge<string | { a: 1 }, { a: 2 }>( string | { a: 1 | 2 })让我们从助手开始弄清楚这种类型。
一想到工会,我就有一种暗示,这种类型会变得复杂。TypeScript 没有很好的内置方法来测试类型相等性,但是我们可以编写一个辅助类型,如果两种类型不相等,则会导致编译器错误。
(注意:Test类型可以改进,它可以允许不等价的类型通过,但对于我们在这里的使用来说已经足够了,同时保持非常简单)
type Pass = 'pass';
type Test<T, U> = [T] extends [U]
? [U] extends [T]
? Pass
: { actual: T; expected: U }
: { actual: T; expected: U };
function typeAssert<T extends Pass>() {}
Run Code Online (Sandbox Code Playgroud)
我们可以像这样使用这个助手:
// try changing Partial to Required
typeAssert<Test<Partial<{ a: 1 }>, { a?: 1 }>>();
Run Code Online (Sandbox Code Playgroud)
接下来,我们需要两种辅助类型。一种用于获取对象的所有必需键,另一种用于获取可选键。首先,一些测试来描述我们所追求的:
typeAssert<Test<RequiredKeys<never>, never>>();
typeAssert<Test<RequiredKeys<{}>, never>>();
typeAssert<Test<RequiredKeys<{ a: 1; b: 1 | undefined }>, 'a' | 'b'>>();
typeAssert<Test<OptionalKeys<never>, never>>();
typeAssert<Test<OptionalKeys<{}>, never>>();
typeAssert<Test<OptionalKeys<{ a?: 1; b: 1, c: undefined }>, 'a'>>();
Run Code Online (Sandbox Code Playgroud)
这里有两点需要注意。首先,*Keys<never>是never。这很重要,因为我们稍后将在联合中使用这些助手,并且如果对象是never它不应该提供任何键。其次,这些测试都不包括联合检查。考虑到我说工会的重要性,这可能会让您感到惊讶。但是,这些类型仅在分配所有联合之后使用,因此它们在那里的行为无关紧要(尽管如果您将这些包含在您的项目中,您可能想要查看所述行为,这与您可能期望的不同RequiredKeys由于它的书写方式)
这些类型通过了给定的检查:
type OptionalKeys<T> = {
[K in keyof T]-?: T extends Record<K, T[K]> ? never : K;
}[keyof T;
type RequiredKeys<T> = {
[K in keyof T]-?: T extends Record<K, T[K]> ? K : never;
}[keyof T] & keyof T;
Run Code Online (Sandbox Code Playgroud)
关于这些的夫妇注意事项:
-?到的属性删除可选性,这让我们避免的包装Exclude<..., undefined>T extends Record<K, T[K]>因为工作{ a?: 1 }没有不延长{ a: 1 | undefined }。在最终解决这个问题之前,我经历了几次迭代。您还可以像jcalz 在这里所做的那样检测其他映射类型的可选性。OptionalKeys可分配给keyof T。但是,它无法检测到相同的RequiredKeys. 与相交keyof T修复了这个问题。现在我们有了这些助手,我们可以定义另外两种代表您的业务逻辑的类型。我们需要RequiredMergeKeys<T, U>和OptionalMergeKeys<T, U>。
type RequiredMergeKeys<T, U> = RequiredKeys<T> & RequiredKeys<U>;
type OptionalMergeKeys<T, U> =
| OptionalKeys<T>
| OptionalKeys<U>
| Exclude<RequiredKeys<T>, RequiredKeys<U>>
| Exclude<RequiredKeys<U>, RequiredKeys<T>>;
Run Code Online (Sandbox Code Playgroud)
以及一些测试以确保这些行为符合预期:
typeAssert<Test<OptionalMergeKeys<never, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<never, { a: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<never, { a?: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<{}, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<{ a: 1 }, { b: 2 }>, 'a' | 'b'>>();
typeAssert<Test<OptionalMergeKeys<{}, { a?: 1 }>, 'a'>>();
typeAssert<Test<RequiredMergeKeys<never, never>, never>>();
typeAssert<Test<RequiredMergeKeys<never, {}>, never>>();
typeAssert<Test<RequiredMergeKeys<never, { a: 1 }>, never>>();
typeAssert<Test<RequiredMergeKeys<{ a: 0 }, { a: 1 }>, 'a'>>();
Run Code Online (Sandbox Code Playgroud)
现在我们有了这些,我们可以定义两个对象的合并,暂时忽略基元和联合。这将调用Merge我们尚未定义的顶级类型来处理成员的原语和联合。
type MergeNonUnionObjects<T, U> = {
[K in RequiredMergeKeys<T, U>]: Merge<T[K], U[K]>;
} & {
[K in OptionalMergeKeys<T, U>]?: K extends keyof T
? K extends keyof U
? Merge<Exclude<T[K], undefined>, Exclude<U[K], undefined>>
: T[K]
: K extends keyof U
? U[K]
: never;
};
Run Code Online (Sandbox Code Playgroud)
(我没有在这里写具体的测试,因为我有它们用于下一个级别)
我们需要处理联合和非对象。接下来让我们处理对象的联合。根据前面的讨论,我们需要分布在所有类型上并单独合并它们。这很简单。
type MergeObjects<T, U> = [T] extends [never]
? U extends any
? MergeNonUnionObjects<T, U>
: never
: [U] extends [never]
? T extends any
? MergeNonUnionObjects<T, U>
: never
: T extends any
? U extends any
? MergeNonUnionObjects<T, U>
: never
: never;
Run Code Online (Sandbox Code Playgroud)
请注意,我们对[T] extends [never]和进行了额外检查[U] extends [never]。这是因为never在分配条款是喜欢for (let i = 0; i < 0; i++),它永远不会进入条件的“身体”,因此会返回never,但我们只希望never,如果这两种类型never。
我们快到了!我们现在可以处理合并对象,这是这个问题最难的部分。剩下的就是处理原语,我们可以通过形成所有可能原语的联合并将原语排除到传递给 的类型来完成MergeObjects。
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type Merge<T, U> =
| Extract<T | U, Primitive>
| MergeObjects<Exclude<T, Primitive>, Exclude<U, Primitive>>;
Run Code Online (Sandbox Code Playgroud)
有了这种类型,我们就完成了!Merge行为符合预期,只有 50 行左右的未注释的疯狂。
……还是我们?@petroni 在评论中提到,这种类型不适用于两个对象中都存在的数组。有几种不同的方法来处理这个问题,特别是因为 TypeScript 的元组类型变得越来越灵活。正确合并[1, 2],[3]可能应该放弃[1 | 3, 2?]……但这样做至少和我们已经做过的一样复杂。一个更简单的解决方案是完全忽略元组,并始终生成一个数组,因此此示例将生成(1 | 2 | 3)[].
关于生产类型的最后说明:
现在的结果类型Merge是正确的,但它的可读性不如预期。现在将鼠标悬停在结果类型上将显示一个交叉点和内部对象,Merge它们已经环绕它们而不是显示结果。我们可以通过引入Expand强制 TS 将所有内容扩展为单个对象的类型来解决此问题。
type Expand<T> = T extends Primitive ? T : { [K in keyof T]: T[K] };
Run Code Online (Sandbox Code Playgroud)
现在只需修改MergeNonUnionObjects为调用Expand. 这是必要的地方有点反复试验。您可以尝试包含或不包含它,以获得适合您的类型显示。
type MergeNonUnionObjects<T, U> = Expand<
{
[K in RequiredMergeKeys<T, U>]: Expand<Merge<T[K], U[K]>>;
} & {
[K in OptionalMergeKeys<T, U>]?: K extends keyof T
? K extends keyof U
? Expand<Merge<
Exclude<T[K], undefined>,
Exclude<U[K], undefined>
>>
: T[K]
: K extends keyof U
? U[K]
: never;
}
>;
Run Code Online (Sandbox Code Playgroud)
在操场上查看它,其中包括我用来验证结果的所有测试。
| 归档时间: |
|
| 查看次数: |
3467 次 |
| 最近记录: |