TypeScript联合类型在switch语句中的穷尽性检查
在开发中,我们经常需要根据一个变量的不同取值(即联合类型)执行不同的逻辑。使用 switch 语句时,如果忘记处理某种情况,程序可能会在运行时产生不可预知的行为。TypeScript 提供了一种利用 never 类型进行“穷尽性检查”的技巧,可以在编译阶段就发现这种遗漏。
1. 定义基础场景与问题
定义 一个表示状态的联合类型 State,包含 Pending、Success 和 Fail 三种状态。
type State = 'Pending' | 'Success' | 'Fail';
编写 一个处理该状态的函数 processState,接收 state 参数。
function processState(state: State): string {
switch (state) {
case 'Pending':
return '处理中...';
case 'Success':
return '操作成功';
// 注意:这里故意遗漏了 'Fail' 的情况
}
}
此时,TypeScript 编译器可能不会报错,函数隐式返回了 undefined。如果在后续代码中强依赖返回的字符串,就会导致 Bug。
2. 使用 never 类型进行穷尽性检查
添加 一个 default 分支,并将 state 赋值给一个类型为 never 的常量。
function processState(state: State): string {
switch (state) {
case 'Pending':
return '处理中...';
case 'Success':
return '操作成功';
case 'Fail':
return '操作失败';
default:
// 核心检查逻辑
const exhaustiveCheck: never = state;
return exhaustiveCheck;
}
}
理解 其背后的原理:
TypeScript 的类型控制流分析会根据 switch 的 case 分支缩小 state 的类型范围。
- 当所有
case(Pending,Success,Fail)都被处理后,进入default分支时,TypeScript 判断state已经没有可能的剩余值,因此它的类型被缩小为never(即“不可能存在”的类型)。 - 将
state赋值给never类型的exhaustiveCheck是合法的。
如果此时我们修改 State 类型,添加一个新的状态 'Unknown'。
type State = 'Pending' | 'Success' | 'Fail' | 'Unknown';
TypeScript 会立即在 default 分支报错:
Type 'string' is not assignable to type 'never'.
这是因为 switch 没有处理 'Unknown',所以进入 default 时,state 的类型变成了 'Unknown',而 'Unknown' 无法赋值给 never。这个错误强制开发者修正代码,补全 case 分支。
3. 提取为辅助函数(最佳实践)
为了在多个地方复用该逻辑,避免每次都手写 const check: never = ...,我们可以封装一个辅助函数。
创建 一个名为 assertNever 的函数:
function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x);
}
重构 processState 函数,在 default 中调用它:
function processState(state: State): string {
switch (state) {
case 'Pending':
return '处理中...';
case 'Success':
return '操作成功';
case 'Fail':
return '操作失败';
default:
return assertNever(state);
}
}
当再次发生类型遗漏时,TypeScript 同样会报错。因为 assertNever 的参数要求是 never,而实际的 state 包含未处理的枚举值,类型不匹配。
4. 类型检查流程解析
为了更直观地理解 TypeScript 如何在代码执行流中判断类型,请参考以下逻辑流程:
匹配 'A'?} B -- 是 --> C["执行逻辑 A\n剩余类型: 'B'"] B -- 否 --> C C --> D{Switch Case
匹配 'B'?} D -- 是 --> E["执行逻辑 B\n剩余类型: never"] D -- 否 --> E E --> F["Default 分支赋值"] F --> G{剩余类型是否为 never?} G -- 是 --> H["编译通过"] G -- 否 --> I["编译报错\n类型不匹配"]
5. 处理对象类型的联合
这种技巧同样适用于对象类型的联合,而不仅是字符串字面量。
定义 一个 Shape 联合类型,包含 Circle 和 Square。
type Circle = {
kind: 'circle';
radius: number;
};
type Square = {
kind: 'square';
sideLength: number;
};
type Shape = Circle | Square;
编写 计算面积的函数,并应用穷尽性检查:
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
default:
const exhaustiveCheck: never = shape;
return exhaustiveCheck;
}
}
如果未来添加一个新的形状 Triangle 到 Shape 类型中:
type Triangle = {
kind: 'triangle';
base: number;
height: number;
};
type Shape = Circle | Square | Triangle;
getArea 函数中的 default 分支会立即报错,提示你处理 Triangle 的逻辑。
6. 方案对比总结
| 检查方式 | 实现难度 | 报错时机 | 可维护性 |
|---|---|---|---|
| 人工检查 | 低 | 运行时(可能导致错误) | 差,易遗漏 |
| ESLint 规则 | 中(需配置) | 编译时(Lint 阶段) | 中,依赖团队配置 |
| never 类型检查 | 低 | 编译时(类型检查阶段) | 优,原生支持,自动触发 |

暂无评论,快来抢沙发吧!