TypeScript合并类型:交集属性必填、差集属性可选的实现问题
问题描述
我们有一个返回结构略有差异数据的API,需要将多个TypeScript类型合并为一个定义:两类共有的属性标记为必填,非重叠属性标记为可选。
给定输入类型X和Y,期望得到Combined类型(见示例)。现有DeepMerge工具类型已接近实现,但存在以下问题:
- 数组类型赋值时TypeScript报错;
- 可选属性额外带有
| undefined类型; - 泛型函数约束时出现问题。
输入类型
interface X { id: number; nonOptionalObject: { id: number; optional: string }; foo: { bar: boolean }; array: { hello: string }[]; //bonus: string; } interface Y { id: number; additional: string; nonOptionalObject: { id: number; }; array: { hello: string }[]; //bonus: number; }
期望合并结果
interface Combined{ id: number; additional?: string; nonOptionalObject: { id: number; optional?: string } foo?: { bar: boolean; } array:{ hello: string; }; //bonus: string | number }
现有工具代码及问题
type Merge<T, U> = Partial<T & U> & (T | U); type DeepMerge<T, U> = T extends object ? U extends object ? { [K in keyof Merge<T, U>]: K extends keyof T ? K extends keyof U ? Exclude<DeepMerge<Exclude<T[K], undefined>, Exclude<U[K], undefined>>, undefined> : Exclude<T[K], undefined> : K extends keyof U ? Exclude<U[K], undefined> : never; } : T | U : T | U; type mergedDefinition = DeepMerge<X, Y>; // 问题1:可选属性额外带| undefined,体验不佳 const bookingResponse: mergedDefinition = { id: 0, nonOptionalObject: { id: 1 }, // 问题2:数组赋值时报错 array: [] }; // 访问bookingResponse.id的类型是正常的 // 问题3:泛型函数约束报错 const genericFunc = <T extends mergedDefinition = mergedDefinition>() => { }; genericFunc<X>();
修正后的DeepMerge工具类型
针对上述问题,调整类型逻辑后可修复所有问题:
// 提取两个类型的共有键 type CommonKeys<T, U> = keyof T & keyof U; // 提取仅在T中存在的键 type OnlyTKeys<T, U> = Exclude<keyof T, keyof U>; // 提取仅在U中存在的键 type OnlyUKeys<T, U> = Exclude<keyof U, keyof T>; // 深度合并单个属性的类型 type DeepMergeProp<T, U> = T extends object | undefined ? U extends object | undefined ? DeepMerge<NonNullable<T>, NonNullable<U>> : U : U extends object | undefined ? T : T | U; // 最终的深度合并类型 type DeepMerge<T, U> = // 共有属性:必填,深度合并 { [K in CommonKeys<T, U>]-?: DeepMergeProp<T[K], U[K]> } & // 仅T有的属性:可选,移除不必要的undefined { [K in OnlyTKeys<T, U>]?: T[K] extends infer V ? Exclude<V, undefined> : never } & // 仅U有的属性:可选,移除不必要的undefined { [K in OnlyUKeys<T, U>]?: U[K] extends infer V ? Exclude<V, undefined> : never } // 展开交叉类型,优化类型显示 extends infer O ? { [K in keyof O]: O[K] } : never;
修正效果验证
type mergedDefinition = DeepMerge<X, Y>; // 验证1:数组赋值正常 const bookingResponse: mergedDefinition = { id: 0, nonOptionalObject: { id: 1 }, array: [] }; // 验证2:可选属性无多余的| undefined type CheckAdditional = mergedDefinition["additional"]; // string | undefined(属性为可选,符合预期,类型提示不会冗余显示) type CheckFoo = mergedDefinition["foo"]; // { bar: boolean } | undefined // 验证3:泛型约束正常 const genericFunc = <T extends mergedDefinition = mergedDefinition>() => { }; genericFunc<X>(); // 无报错
修复说明
- 数组类型问题:通过
NonNullable<T>确保合并数组时保留原始结构,避免Exclude对数组类型的错误处理。 - 可选属性冗余undefined:将非共有属性标记为可选(
?:),同时移除原类型中多余的undefined,避免出现属性?: string | undefined的冗余情况。 - 泛型约束问题:通过明确拆分共有键和独有键的结构,确保合并后的类型能正确兼容原类型X和Y,解决约束不兼容问题。
内容的提问来源于stack exchange,提问作者Kilian
相关产品推荐
相关产品推荐

