TypeScript是否支持对象类型的真正XOR异或联合类型?
TS的联合类型|是常规的OR逻辑,只要值满足联合类型中任意一个成员的类型约束,就会判定为合法。同时TS的对象类型默认允许存在不在类型定义中的额外属性,仅字面量直接赋值时的「新鲜对象检查」会校验是否存在所有联合成员都不包含的未知属性,不会校验部分存在的可选组属性。
第一个示例中你定义的Props = CommonProps | (CommonProps & OptionalProps)里,props2 = {a:1, b:2, c:3}完全满足第一个分支CommonProps的约束(有a、b两个必填属性,额外的c不属于所有联合成员都不包含的未知属性,不会触发新鲜对象检查报错),因此TS判定为合法。
你之前尝试的Record<keyof OptionalProps, never>写法不生效的原因是:Record生成的是必填的c:never、d:never类型,要求这两个属性必须存在且值为never(没有任何合法值可以满足该约束),因此第一个分支实际上无法被匹配,自然起不到限制作用。
要实现你要的「要么没有可选组全部属性,要么有可选组全部属性」的互斥效果,需要把禁止出现的属性定义为可选+never类型,表示该属性要么不出现,出现则必须是不可能的never类型,间接实现「不能存在该属性」的约束。
第一个示例修复代码
type CommonProps = {a:number, b: number}; type OptionalProps = {c: number, d: number}; // 定义禁止出现可选属性的分支:可选属性全部设为?never type NoOptionalProps = { [P in keyof OptionalProps]?: never } type Props = CommonProps & (OptionalProps | NoOptionalProps) const props1: Props = {a: 1, b: 2}; // 合法 const props2: Props = {a: 1, b: 2, c: 3}; // 报错:对象字面量只能指定已知属性,并且类型“Props”中不存在“c” const props3: Props = {a: 1, b: 2, x: 3}; // 报错 const props4: Props = {a: 1, b: 2, c: 3, d: 4}; // 合法 const props5: Props = {a: 1, b: 2, c: 3, d: 4, e: 5}; // 报错
第二个示例修复代码
要实现两个可选组二选一的互斥效果,同样需要在每个分支中将另一个组的属性设为?never:
type CommonProps = {a:number, b: number}; type OptionalProps1 = {c: number}; type OptionalProps2 = {d: number}; type Props = CommonProps & ( (OptionalProps1 & { [P in keyof OptionalProps2]?: never }) | (OptionalProps2 & { [P in keyof OptionalProps1]?: never }) ) const props1: Props = {a: 1, b: 2}; // 报错 const props2: Props = {a: 1, b: 2, c: 3}; // 合法 const props3: Props = {a: 1, b: 2, x: 3}; // 报错 const props4: Props = {a: 1, b: 2, c: 3, d: 4}; // 报错 const props5: Props = {a: 1, b: 2, c: 3, d: 4, e: 5}; // 报错
通用互斥工具类型
可以封装通用的XOR工具类型简化写法:
// 把T中属于U的属性全部设为可选never type Without<T, U> = { [P in keyof U]?: never } // 两个类型互斥 type XOR<T, U> = (T & Without<U, T>) | (U & Without<T, U>) // 第一个示例用通用类型写法 type Props = CommonProps & XOR<OptionalProps, Record<string, never>> // 第二个示例用通用类型写法 type Props = CommonProps & XOR<OptionalProps1, OptionalProps2>
按上述方式定义的互斥联合类型,在读取属性时也会触发TS的类型收窄:比如判断if ('c' in props),TS会自动把props的类型收窄到带OptionalProps的分支,此时可以正常读取c、d属性;如果判断为否,则会收窄到无可选属性的分支,读取c、d会直接报错,符合你的需求。
内容的提问来源于stack exchange,提问作者0x24a537r9

