先记住这个答案
判别联合把不同状态分别定义为对象成员,并用公共字段中的可区分取值建立标签与成员的关联。常见写法是 kind 为不同字符串字面量,分支各自拥有必需数据;检查 kind 后,TypeScript 就能选出对应成员。若每个标签都写成宽 string,某个具体字符串无法排除其他成员,专有字段便不能按预期使用。把所有数据放进同一对象并设成可选,也没有表达状态与字段同时成立的关系。设计时应让真正互斥的状态在类型中保持互斥。
- 每个成员绑定自己的标签与必需字段
- 宽 string 标签无法提供唯一的分支关联
- 可选字段大对象会允许无意义的状态组合
为什么要拆成多个对象成员
下载完成必然有文件地址,下载失败必然有原因。如果只写一个状态字段,再把地址与原因都声明成可选,类型会允许完成却没有地址的对象。使用者即使检查状态,也无法从这份模型中证明可选字段已经存在。
示例把两种状态拆开,再用公共 kind 关联数据。分支里可以直接使用必需字段,构造缺少地址的完成状态会被拒绝。这样类型约束同时服务生产者和消费者,避免只有读取端反复断言字段存在,而创建端仍能构造错误组合。
type Download =
| { kind: 'done'; url: string }
| { kind: 'failed'; reason: string };
function summary(value: Download): string {
switch (value.kind) {
case 'done': return value.url;
case 'failed': return value.reason;
}
}
const finished: Download = { kind: 'done', url: '/report.pdf' };
// @ts-expect-error 完成状态必须提供 url
const broken: Download = { kind: 'done' };标签判断选中整个成员,因此 done 分支拥有必需 url,failed 分支拥有必需 reason。缺少地址的完成对象在创建时就报错,不能靠标签看起来正确而绕过负载契约。
标签拓宽为什么会丢失关联
如果两个成员的 kind 都是 string,它们都可能在运行时等于 done,检查这个值就不能证明当前是哪一支。常见诱因是先创建可变对象,标签属性被推断成 string,随后再尝试传入有限联合。应在构造边界提供适当注解或保留字面量。
即使使用字面量,如果多个成员共享同一个标签值,检查后仍可能剩下多个候选,专有字段也未必全部可用。标签不必拘泥于字符串,但必须能为所需区分提供足够证据。不要把某个固定三条件口诀当作所有高级联合行为的完整规范。
从接口数据到状态模型的边界
外部响应中的标签和字段仍需要验证,服务端返回 done 却没有地址时,静态声明不会替你补齐。可以在解析层把协议映射为受控内部联合,并为未知状态设计明确失败或兼容路径;进入内部逻辑后再依赖分支收窄。
状态增加时,还应检查所有消费者是否补齐处理。显式返回契约可以发现部分遗漏,never 穷尽检查则能直接指出尚未处理的成员。对于嵌套流程,应在每个状态层级保存对应负载,不要让一个标签承担所有彼此独立状态的组合。
容易答错的地方
- 只要有 kind 字段就是判别联合
- 如果标签是无法区分成员的宽类型,或所有负载都散落在可选字段中,检查标签并不能自动建立完整的状态与数据关联。
- 每个标签字面量必须只对应一个字段
- 标签对应的是整个成员结构,可以携带多项必需数据;若同一标签被多个成员共享,收窄后可能仍保留那些成员的联合。
面试官还会怎么问?
标签能使用 true 和 false 吗?
可以用布尔字面量区分两个成员,但必须把对应数据分别绑定到两支;仅有 boolean 和两个可选字段的大对象仍未表达关联。
为什么先存变量再传入时标签会报错?
对象属性可能因为可变性推断成 string,超出目标联合允许的字面量集合;应在创建边界用明确类型或合适的常量推断保留状态信息。
可以用标签完全替代运行时校验吗?
不能,外部对象可能带正确标签却缺少对应字段;解析层需要验证成员完整性,之后内部状态处理才能可靠依赖这个联合契约。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。