如何通过 “never” 类型防止生产环境中的代码出错

发布: (2026年1月6日 GMT+8 15:51)
10 min read
原文: Dev.to

Source: Dev.to

如何使用 never 类型防止生产环境代码出错

在 TypeScript 中,never 类型经常被误解为“没有值”。实际上,它代表 永远不会出现的值——即函数永远不会正常返回,或者代码路径永远不可能被执行。正确使用 never 可以让编译器在编译阶段捕获潜在的运行时错误,从而避免在生产环境中出现意外的崩溃。

下面我们将通过几个实际案例来说明 never 的作用以及如何在项目中加以利用。


目录

  1. 什么是 never 类型?
  2. 使用 never 实现穷尽检查(Exhaustive Checks)
  3. 防止不可达代码(Unreachable Code)
  4. 在抛异常函数中使用 never
  5. 在泛型约束中使用 never
  6. 结论

什么是 never 类型?

  • never 表示 不可能出现的值。常见的场景包括:

    • 永远抛出异常的函数
    • 永远进入无限循环的函数
    • 通过类型系统保证的“不可达”分支
  • void 不同,void 表示函数 可能不返回任何值(即返回 undefined),而 never 表示函数 根本不会返回


使用 never 实现穷尽检查(Exhaustive Checks)

在处理 受限集合(如枚举或联合类型)时,使用 never 可以让 TypeScript 在新增成员时提示我们更新相应的分支逻辑。

type Action =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number }
  | { type: "reset" };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "increment":
      return state + action.amount;
    case "decrement":
      return state - action.amount;
    case "reset":
      return 0;
    default:
      // 这里的 `action` 的类型会被推断为 `never`
      // 如果以后在 `Action` 中添加了新成员,编译器会报错
      const _exhaustiveCheck: never = action;
      return _exhaustiveCheck;
  }
}

要点

  • default 分支中将 action 赋值给 never 类型的变量 _exhaustiveCheck
  • Action 联合类型新增成员时,action 将不再是 never,编译器会报错提醒我们处理新分支。

防止不可达代码(Unreachable Code)

有时我们会在代码中使用 assertNever 辅助函数来显式标记“不可能到达”的路径。

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

// 示例
type Shape = { kind: "circle"; radius: number } | { kind: "square"; size: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.radius ** 2;
    case "square":
      return s.size ** 2;
    default:
      return assertNever(s); // 如果以后添加了新形状,这里会报错
  }
}
  • assertNever 的返回类型也是 never,这意味着调用它的代码路径 永远不会继续执行,从而帮助我们捕获遗漏的分支。

在抛异常函数中使用 never

当函数的唯一目的就是抛出错误时,使用 never 可以让调用者的类型推断更加精准。

function fail(message: string): never {
  throw new Error(message);
}

// 用法示例
function getUser(id: string) {
  const user = database.find(u => u.id === id);
  if (!user) {
    return fail(`User with id ${id} not found`);
  }
  return user;
}
  • fail 返回 never,所以 getUser 的返回类型会自动推断为 User(而不是 User | undefined),从而消除不必要的空值检查。

在泛型约束中使用 never

never 还能帮助我们在 条件类型 中实现更严格的约束。

type IsNever<T> = [T] extends [never] ? true : false;

// 示例
type Test1 = IsNever<never>; // true
type Test2 = IsNever<string>; // false
  • 通过把待检查的类型包装在元组中 ([T]) 可以防止分配式条件类型的分配行为,从而得到准确的判断。

结论

  • never 并不是“空”或“未定义”,而是 永远不可能出现的类型
  • 利用 never 可以在 编译阶段 捕获:
    • 未处理的联合类型成员
    • 不可达代码路径
    • 永不返回的函数
  • 在大型代码库中引入 never 的穷尽检查,可显著降低因忘记处理新分支而导致的生产环境错误。

通过在合适的地方使用 never,我们可以让 TypeScript 成为更强大的防御性工具,让代码在 写入生产之前 就已经被“自检”了一遍。祝你在项目中玩转 never,写出更安全、更可靠的代码!

什么是 never 类型?

在 TypeScript 中,never 表示 不可能发生的事
不是 “返回空” 或 “空值”。它表示一条不可能到达的代码路径。

如果一个值的类型是 never,这意味着:

  • 没有任何可能的运行时值
  • 如果执行到这里,说明出现了错误

简单示例

function crash(): never {
  throw new Error("Boom");
}

此函数永远不会正常结束。

function infiniteLoop(): never {
  while (true) {}
}

执行永远不会在此之后继续,因此返回类型为 never

never 强大的唯一规则

never 可以赋值给所有类型,但没有任何类型(never 本身除外)可以赋值给 never

let x: never;

x = "hello"; // ❌ error
x = 123;     // ❌ error

如果 TypeScript 发现有 真实值 被赋给 never,构建将会失败。我们将利用这条规则来阻止生产环境的 bug。

设置:常见的 React 模式

想象一个组件,根据 type 字段渲染 UI 块(CMS 块、功能标记、工作流——非常常见)。

type Block =
  | { type: "hero"; title: string }
  | { type: "cta"; text: string };

渲染器

function RenderBlock({ block }: { block: Block }) {
  switch (block.type) {
    case "hero":
      return <h2>{block.title}</h2>;

    case "cta":
      return <div>{block.text}</div>;
  }
}

一切看起来都正常:

  • TypeScript 很开心
  • CI 通过
  • 没有运行时错误

生产环境 Bug(静默)

type Block =
  | { type: "hero"; title: string }
  | { type: "cta"; text: string }
  | { type: "banner"; image: string };

他们忘记更新 RenderBlock。会发生什么?

  • 没有 TypeScript 错误
  • 没有构建失败
  • 组件返回 undefined → 什么也不渲染

生产 UI 静默破坏。 这是最糟糕的 bug 类型。

为什么 TypeScript 没有拯救你

从 TypeScript 的角度来看,函数可能返回 JSX.Element | undefined,这在技术上是有效的。TypeScript 并不假设 switch 已经穷尽所有情况,因此错误仍然会通过。

错误的“修复”

像这样添加一个 default 分支:

default:
  return null;

仍然留下问题:

  • 没有编译器错误
  • UI 仍然损坏
  • 问题变得更难被注意到

我们需要 TypeScript 大声报错

介绍 never

更改组件中的 一行 代码:

function RenderBlock({ block }: { block: Block }) {
  switch (block.type) {
    case "hero":
      return <h2>{block.title}</h2>;

    case "cta":
      return <div>{block.text}</div>;

    default:
      const _exhaustiveCheck: never = block;
      return null;
  }
}

结果

穷尽检查错误截图

关键行是:

const _exhaustiveCheck: never = block;

有哪些更改?

此行告诉 TypeScript:“如果 block 到达这里,代码就是错误的。”
banner 存在时,TypeScript 看到 block 可以是 { type: "banner" },这 not assignable to never,从而产生编译时错误。CI 失败,错误永远不会被发布。

真正的收益

它不仅仅是关于穷尽性检查;它提供了以下保证:

如果代码能够编译,则所有情况都已处理。
这就是生产安全。

为什么在真实代码库中这很重要

该模式在以下情况下表现突出:

  • CMS 架构演进
  • 多个团队共同使用相同的联合类型
  • UI 渲染依赖于配置或 API 响应
  • Reducer 或工作流随时间增长

只要 类型变化快于实现never 就能保护你。

心智模型

never 的含义是:“此代码路径不应存在。”
如果 TypeScript 证明它 可以 存在,则构建会失败——这正是设计如此。

结论

  • TypeScript 默认不保证穷尽处理。
  • React 组件在生产环境中可能悄然出错。
  • never 将缺失的情况转化为编译错误。

一行代码即可防止整类 bug:

const _exhaustiveCheck: never = value;

在任何沉默可能危险的地方使用它。

Back to Blog

相关文章

阅读更多 »

React 组件中的 TypeScript 泛型

介绍:泛型并不是在 React 组件中每天都会使用的东西,但在某些情况下,它们可以让你编写既灵活又类型安全的组件。

我常用的全栈/前端项目模式

我在几乎每个项目中都会使用的模式——在完成了相当多的前端和全栈项目(主要是 React + TypeScript 加上一些服务器/后端技术)之后。