在某些情况下,我想“合并”默认类型联接(即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;
};
};
我正在寻找一个可以接受2个通用类型的Merge
函数
type Merge<T, U> = ?????;
然后,如果在类型A
和B
上使用,输出将如下所示
type AB = {
a: string;
b: number | boolean;
c?: boolean;
d: {
a2: string;
b2?: number;
c2?: boolean;
};
e?: number;
};
如下所示,Merge
类型将执行以下逻辑:
T
和U
上并且是相同的类型,请根据需要对其进行标记,并在T
/ U
中将其设置为该类型(就像属性a
发生的情况一样。)T
和U
上,但类型不同,则将其标记为必需,如果它是原始类型,则将其设置为联合类型(如属性{{1 }}),或者如果它是一个对象则进行递归合并(如属性b
发生的合并)。d
以及c
和b2
)。c2
中发生的情况)假定您可以使用递归条件类型,尽管我知道它们为aren't yet officially supported,因此不应在生产中使用。我可以为生产用例制作类似于jcalz @的解决方案here的展开版本。
Here is a playground设置了要测试的问题。
答案 0 :(得分:7)
TLDR:魔术!尝试Playground
所以,这是一个棘手的问题。不是因为合并要求,而是因为边缘情况。得到低垂的果实只需要不到20分钟的时间。确保它在任何地方都有效需要花费更多的时间...并使长度增加了两倍。工会很棘手!
什么是可选属性?在{ 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>() {}
我们可以像这样使用此助手:
// try changing Partial to Required
typeAssert<Test<Partial<{ a: 1 }>, { a?: 1 }>>();
接下来,我们将需要两种帮助程序类型。一种获取对象的所有必需键,另一种获取可选键。首先,进行一些测试以描述我们的追求:
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'>>();
这里有两件事要注意。首先,*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;
有关这些的注释:
-?
删除属性的可选性,这使我们避免使用Exclude<..., undefined>
的包装器T extends Record<K, T[K]>
之所以有效,是因为{ a?: 1 }
不会 扩展{ a: 1 | undefined }
。我经历了几次迭代,然后才最终解决。您还可以检测其他映射类型为jcalz does here的可选性。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>>;
进行一些测试以确保它们的行为符合预期:
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'>>();
现在有了这些,我们可以定义两个对象的合并,暂时不考虑基元和联合。这将调用我们尚未定义的顶级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;
};
(我在这里没有写特定的测试,因为我将其用于下一级别的考试)
我们需要同时处理联合和非对象。接下来让我们处理对象的并集。根据前面的讨论,我们需要分布在所有类型上并分别合并它们。这非常简单。
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;
请注意,我们对[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>>;
有了这种类型,我们就完成了! Merge
表现良好,只有50行左右没有注释。
关于生产类型的最后说明:
目前从Merge
得到的类型是正确的,但它的可读性不尽如人意。现在,将鼠标悬停在结果类型上将显示交集和内部对象,并且内部包裹着Merge
,而不显示结果。我们可以通过引入Expand
类型来解决此问题,该类型强制TS将所有内容扩展为单个对象。
type Expand<T> = T extends Primitive ? T : { [K in keyof T]: T[K] };
现在只需修改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;
}
>;
在the playground中进行检查,其中包括我用来验证结果的所有测试。