TypeScript如何实现无冗余的多类型互斥联合类型?
TypeScript 无冗余实现互斥联合类型
我在TypeScript中定义了如下接口:
interface IPositionChange { previousIndex: number; newIndex: number; } interface IVisibilityChange { index: number; panel: string; } interface IAction { action: 'paint' | 'clean'; } interface IActionChange extends IAction, IVisibilityChange { }
我需要定义一个互斥联合类型IChange,要求它的实例只能属于IPositionChange、IVisibilityChange、IActionChange中的某一种,不能同时包含多种类型的属性。但我尝试的几种IChange定义方式都没达到效果:在函数thisShouldNotCompile中,混合多种类型属性的赋值没有触发编译错误。
我发现给每个接口手动添加其他类型的never属性可以实现互斥,但这种方式代码冗余严重,后续扩展非常麻烦:
interface IPositionChange { previousIndex: number; newIndex: number; action: never; index: never; panel: never; } interface IVisibilityChange { index: number; panel: string; previousIndex: never; newIndex: never; action: never; } interface IAction { action: 'paint' | 'clean'; index: never; panel: never; previousIndex: never; newIndex: never; }
问题分析:普通联合类型为什么不生效?
TypeScript的普通联合类型(比如type IChange = IPositionChange | IVisibilityChange | IActionChange)的检查逻辑是只要对象满足联合类型中某一个成员的必填属性要求,就会被判定为合法,即使它包含其他成员的属性。
举个例子:一个同时包含previousIndex、newIndex、index的对象,既满足IPositionChange的要求(有previousIndex和newIndex),也满足IVisibilityChange的要求(有index和panel),所以TypeScript会认为它属于IChange,不会触发编译错误——这就是核心问题所在。
无冗余的互斥联合类型实现方案
可以通过工具类型自动生成互斥约束,不需要手动修改原接口,扩展性更强:
- 先定义基础的联合类型:
type BaseChange = IPositionChange | IVisibilityChange | IActionChange;
- 实现一个工具类型,自动给联合类型的每个成员添加其他成员属性的
never约束:
type ExclusifyUnion<T, U = T> = T extends unknown ? T & Partial<Record<Exclude<keyof U, keyof T>, never>> : never;
- 用这个工具类型生成最终的
IChange:
type IChange = ExclusifyUnion<BaseChange>;
原理说明
- 分布式条件类型
T extends unknown会遍历联合类型BaseChange的每个成员; Exclude<keyof U, keyof T>会取出当前成员T以外的所有其他成员的属性名;Record<..., never>把这些属性名映射为never类型,Partial让它们变为可选,但never类型不接受任何赋值,所以只要对象包含其他类型的属性,就会触发编译错误;- 最后通过交叉类型
T & ...,把当前成员的属性和其他成员的never属性合并,实现互斥。
验证效果
现在再写混合属性的代码,就会触发编译错误:
function thisShouldNotCompile() { // 以下代码会报错:类型“number”不能赋值给类型“never” const invalidChange: IChange = { previousIndex: 0, newIndex: 1, index: 2 // 这里会报错,因为IPositionChange对应的互斥类型中index是never }; }
内容的提问来源于stack exchange,提问作者Mario Figueiredo
相关产品推荐
相关产品推荐

