mcpField
E
exhaustiveness-checking

exhaustiveness-checking

github.com/marius-townhouse开发651 次安装39 周前更新
代码仓库

关于 exhaustiveness-checking

exhaustiveness-checking 是一项 TypeScript 技能,讲解如何用 never 类型在编译时发现未处理的联合类型变体,帮助开发者在类型变化时保持 switch 分支完整。

主要功能

  1. 01用 never 检查分支穷尽性
  2. 02在 switch 默认分支使用 assertUnreachable
  3. 03编译时发现遗漏分支
  4. 04通过显式返回类型强制覆盖
  5. 05识别有意采用部分处理的情况

适用场景

  1. 01处理标记联合类型的所有变体
  2. 02新增联合类型分支后更新 switch
  3. 03避免 TypeScript 代码静默遗漏分支
  4. 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 类型进行穷尽性检查。

评论

支持 Markdown