先记住这个答案
对有限联合逐个处理成员后,剩余分支的值应被收窄为 never,表示在当前类型契约下已经没有合法候选。可以在 switch 的 default 中调用只接收 never 的辅助函数;新增联合成员而漏写 case 时,剩余值不再是 never,编译器就会报错。辅助函数通常直接抛错并返回 never,兼顾运行时异常输入。不要把 default 随意返回空字符串当作穷尽检查,那会让新增状态悄悄进入兜底路径。
- 处理完有限联合后,剩余候选应为 never
- assertNever 参数负责检查遗漏成员
- 运行时抛错用于暴露违反静态契约的输入
default 中为何需要接收 never
普通 default 可以接收任何未匹配状态,直接返回默认文案就会掩盖遗漏。只接收 never 的函数则要求调用位置已经排除了所有合法候选。新增一种状态后,编译器会把它保留在 default 中,从而拒绝这次调用并指出缺少处理。
示例用成功和失败两个成员构造结果类型,每个 case 都返回文案。最后的函数不会正常返回,既向类型系统描述不可达,也在输入违反契约时抛出错误。应把完整联合值传给检查函数,便于错误诊断保留未处理成员的结构。
type Result =
| { kind: 'success'; count: number }
| { kind: 'failure'; reason: string };
function assertNever(value: never): never {
throw new Error('Unhandled result');
}
function describe(result: Result): string {
switch (result.kind) {
case 'success': return 'count=' + result.count;
case 'failure': return result.reason;
default: return assertNever(result);
}
}当前两个合法成员都有返回路径,default 参数可以归约为 never。若给 Result 增加 pending 成员而不补 case,assertNever(result) 应产生编译错误,提示状态处理需要同步更新。
return 和可达性会影响剩余类型
分支只有结束当前路径或形成明确互斥控制流,后续位置才会排除已经处理的候选。写了 case 却继续穿透到后续语句,或把标签转成宽字符串再判断,都可能让编译器无法证明剩余值为空。应查看报错位置实际推断出的类型。
never 也可用于返回类型,例如函数必定抛错或永不正常结束。void 则常用于不关心返回结果的调用契约,并不代表函数无法结束。穷尽辅助函数需要表达不存在正常返回路径,不能靠随意返回 undefined 来伪装这一点。
静态穷尽与未知输入怎样配合
有限联合来自可信内部状态时,编译检查能帮助维护所有处理点;来自网络的字符串则可能不在联合里。不能仅靠 as Result 把外部对象送进分支,再宣称所有运行时输入都已经覆盖。应先解析外部数据,之后再执行内部穷尽处理。
测试可以临时为类型添加一个新成员,确认关键处理函数出现预期诊断;同时以运行时错误输入验证兜底确实暴露异常。这个检查不是要求每个 switch 都抛错,开放协议可能需要明确的 unknown 状态,但它也应作为真实成员参与设计。
容易答错的地方
- 写了 default 就已经覆盖所有状态
- default 只是兜底路径,直接返回成功或空值会隐藏新增成员;穷尽检查需要让剩余类型无法赋给 never 时产生明确编译错误。
- 用 as never 可以修复穷尽检查报错
- 这会强行告诉编译器分支不可达,恰好移除了发现遗漏的机制;应检查新增成员或控制流,而不是断言消除证据。
面试官还会怎么问?
辅助函数为什么还要真的抛出错误?
类型检查会在生成代码后消失,错误断言或外部输入仍可能进入该路径;实际抛错能暴露契约被破坏,而不是返回一个伪造正常值。
开放字符串状态适合直接做穷尽检查吗?
无限字符串集合无法逐一列完,可以先映射到有限内部状态并保留明确未知分支,再对内部联合进行穷尽处理和监控。
只写显式返回类型能发现全部遗漏吗?
配合严格空值检查可能发现缺少返回路径,但不能替代对具体剩余成员的验证;never 检查更直接表达所有联合成员必须处理的意图。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。