关于 exhaustiveness-checking
exhaustiveness-checking 是一项 TypeScript 技能,讲解如何用 never 类型在编译时发现未处理的联合类型变体,帮助开发者在类型变化时保持 switch 分支完整。
主要功能
- 01用 never 检查分支穷尽性
- 02在 switch 默认分支使用 assertUnreachable
- 03编译时发现遗漏分支
- 04通过显式返回类型强制覆盖
- 05识别有意采用部分处理的情况
适用场景
- 01处理标记联合类型的所有变体
- 02新增联合类型分支后更新 switch
- 03避免 TypeScript 代码静默遗漏分支
- 04有意只处理选定分支
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill exhaustiveness-checking安装后在 Claude Code 中输入/exhaustiveness-checking确认已加载。
详细介绍
使用 Never 类型进行穷尽性检查
概述
使用 never 确保联合类型中的所有情况都得到处理。
向联合类型添加新变体时,TypeScript 可以自动标记所有需要更新的 switch 语句。这样可以在编译时捕获遗漏错误。
何时使用此技巧
- 处理带标签联合类型中的所有情况
- 向可辨识联合类型添加新变体
- 编写必须完整覆盖所有情况的
switch语句 - 希望遗漏情况时出现编译时错误
铁律
ALWAYS add exhaustiveness checking to switch statements on union types.
请记住:
- 穷尽所有情况后,类型为
never - 除了
never,没有任何类型可以赋值给never - 遗漏情况会导致类型错误
- 这可以捕获遗漏错误
检测:遗漏情况的问题
如果没有进行穷尽性检查,新增的联合类型变体会被悄无声息地忽略:
type Shape = Box | Circle | Line;
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
// Forgot 'line' - NO ERROR! Lines silently don't draw.
}
}
穷尽性检查模式
assertUnreachable 辅助函数
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
在 switch 语句中使用
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
default:
assertUnreachable(shape);
// If we missed a case, shape won't be 'never' and we get a type error!
}
}
添加新类型时
// Add a new shape type
interface Line {
type: 'line';
start: Coord;
end: Coord;
}
type Shape = Box | Circle | Line; // Added Line
// Now drawShape shows an error:
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box': /* ... */ break;
case 'circle': /* ... */ break;
default:
assertUnreachable(shape);
// ~~~~~
// Argument of type 'Line' is not assignable to parameter of type 'never'
}
}
处理这个新情况即可修复:
function drawShape(shape: Shape, ctx: CanvasRenderingContext2D) {
switch (shape.type) {
case 'box':
ctx.rect(...shape.topLeft, ...shape.size);
break;
case 'circle':
ctx.arc(...shape.center, shape.radius, 0, 2 * Math.PI);
break;
case 'line':
ctx.moveTo(...shape.start);
ctx.lineTo(...shape.end);
break;
default:
assertUnreachable(shape); // Now shape is 'never', no error!
}
}
工作原理
处理完所有情况后,剩余类型为 never:
function processShape(shape: Shape) {
switch (shape.type) {
case 'box': break;
case 'circle': break;
case 'line': break;
default:
shape
// ^? (parameter) shape: never
}
}
如果遗漏了某种情况,类型就不是 never:
function processShape(shape: Shape) {
switch (shape.type) {
case 'box': break;
case 'circle': break;
// (forgot 'line')
default:
shape
// ^? (parameter) shape: Line
}
}
由于 Line 不能赋值给 never,因此会出现类型错误。
替代方案:通过返回类型强制检查
也可以使用返回类型来强制进行穷尽性检查:
function getShapeName(shape: Shape): string {
switch (shape.type) {
case 'box':
return 'Box';
case 'circle':
return 'Circle';
// Missing 'line' - TypeScript error!
// Function lacks ending return statement and return type does not include 'undefined'
}
}
这仅在以下条件下有效:
- 函数具有显式返回类型
- 所有情况都必须返回
- 已启用
strictNullChecks
完整示例
// Types
type Coord = [x: number, y: number];
interface Box {
type: 'box';
topLeft: Coord;
size: Coord;
}
interface Circle {
type: 'circle';
center: Coord;
radius: number;
}
interface Line {
type: 'line';
start: Coord;
end: Coord;
}
type Shape = Box | Circle | Line;
// Helper
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// Usage - guaranteed to handle all shapes
function getArea(shape: Shape): number {
switch (shape.type) {
case 'box':
return shape.size[0] * shape.size[1];
case 'circle':
return Math.PI * shape.radius ** 2;
case 'line':
return 0; // Lines have no area
default:
return assertUnreachable(shape);
}
}
不应使用的情况
有时你确实想要有意忽略某些情况:
function handleCommonShapes(shape: Shape) {
switch (shape.type) {
case 'box':
case 'circle':
// Handle common cases
break;
// Intentionally ignore 'line' - don't add assertUnreachable here
}
}
抗压指南
1. “默认情况会处理”
压力:“我们有默认情况,所以没问题。”
**回应:**新增变体时,静默的默认处理会掩盖错误。
**行动:**在默认分支中使用 assertUnreachable,明确暴露遗漏的情况。
2. “我们会记得更新”
压力:“我们知道该在哪里添加新情况。”
**回应:**人会忘记,但编译器检查不会。
**行动:**让 TypeScript 帮你跟踪。
危险信号——停下来重新考虑
- 对联合类型执行
switch,却没有进行穷尽性检查 - 静默不执行任何操作的默认分支
- 向联合类型添加变体,却不检查所有用法
- “TODO: 处理新情况”注释
常见的自我安慰(都不成立)
| 借口 | 现实 |
|---|---|
| “我们会记得更新” | 你会忘,队友也会忘 |
| “默认情况会处理未知情况” | 它会掩盖新增变体带来的错误 |
“这只是一个 switch” | 联合类型往往会出现在多个 switch 中 |
快速参考
// The pattern
function assertUnreachable(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// In switch statements
switch (union.type) {
case 'a': /* ... */ break;
case 'b': /* ... */ break;
default:
assertUnreachable(union); // Type error if cases missing
}
总结
使用 never 将遗漏情况转化为编译时错误。
处理带标签联合类型时,在默认分支中添加 assertUnreachable(value)。这样,向联合类型添加新变体时,所有处理该联合类型的地方都会产生类型错误,从而在编译时而非运行时捕获遗漏错误。
参考资料
基于 Dan Vanderkam 所著的《Effective TypeScript》,第 59 条:使用 Never 类型进行穷尽性检查。