先记住这个答案
A | B 表示值可以满足 A 或 B,使用前只能依赖当前已证明可用的能力;A & B 表示值必须同时满足两者。对象交叉常常拥有双方要求的字段,因此可接受的对象反而更受限制。联合不等于把所有字段都变成可选,交叉也不等于后写属性覆盖前写属性。发生同名字段冲突时,字段必须同时满足两种类型,可能得到 never,某些判别字段冲突还可能使整个交叉无法成立。
- 联合接受任一分支,分支能力需要收窄后使用
- 交叉要求同时满足约束,不执行后值覆盖
- 类型运算不会在运行时创建或合并对象
为什么联合不能直接读取所有字段
一个函数接收带链接的项目或带动作的项目,运行时不保证当前项目同时拥有两者。联合保留了两种候选,所以访问特有字段前必须判断分支。公共字段只有在所有候选都支持相关操作时,才可以直接按公共契约使用。
示例使用属性存在判断区分两种输入。交叉参数则明确要求同一个对象同时拥有链接与动作,函数可以依次使用它们。区别来自函数承担的输入契约,不是由竖线表示弱检查、与号表示高级检查这样的口号决定。
type LinkItem = { href: string };
type ActionItem = { run: () => void };
function useEither(item: LinkItem | ActionItem) {
if ('href' in item) return item.href;
item.run();
return 'action';
}
function useBoth(item: LinkItem & ActionItem) {
item.run();
return item.href;
}
useEither({ href: '/docs' });
// @ts-expect-error 交叉类型还要求 run
useBoth({ href: '/docs' });只有链接的对象可以满足联合,却不能满足交叉。交叉不会自动补出动作函数,联合分支中的读取则依赖实际判断所建立的类型证据。
交叉为什么不是对象展开
JavaScript 对象展开遇到同名属性通常由后面的值覆盖前面的值,这是运行时构造对象的规则。类型交叉没有覆盖顺序:如果同一个字段同时要求字符串和数字,它必须同时满足两个互不相交的集合,因此该字段可能变成 never。
若想描述先删除旧字段再替换类型,通常应明确使用 Omit 去掉冲突键,再组合新定义。这样的类型设计表达替换意图,也方便审阅者发现契约变化。直接叠加交叉可能把错误拖到后续构造对象或调用函数时才暴露。
业务状态不要用可选字段拼凑联合
把成功数据和失败原因都写成可选字段,会允许成功没有数据、失败却带着成功数据等无意义组合。更清楚的方式是让各个成员分别拥有自己的必需字段,再用具体状态标签关联它们,读取时根据标签收窄。
交叉适合组合确实同时存在的能力,例如实体标识与审计字段,但不能证明服务器真的提供这些数据。外部 JSON 仍需要解析验证;只把响应断言成复杂交叉类型,会让错误看起来消失,实际读取时仍可能遇到缺失字段。
容易答错的地方
- 联合就是把双方字段全部设成可选
- 联合保留成员各自的完整约束,可选字段大对象会允许原本不存在的组合,可能破坏状态与数据之间的重要关联。
- 交叉里后面的字段会覆盖前面的字段
- 类型交叉要求同时满足两边,同名字段不会按书写顺序覆盖;需要替换字段时应显式移除旧键并声明新契约。
面试官还会怎么问?
string 与 number 的交叉有什么值?
在正常类型语义下没有同时属于两者的值,结果为 never;这与字符串或数字的联合允许两类值恰好形成对照。
交叉两个对象一定会让整个类型变成 never 吗?
不会。无冲突字段可以正常组合,普通冲突可能只让某个属性成为 never,判别属性等情况才可能让整个交叉被归约为 never。
联合成员共享字段就能随便调用其方法吗?
还要看共享字段的类型是否支持该操作;字段可能仍是联合类型,只有所有可能值都安全支持的操作才可以直接使用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。