TypeScript 中的 Never 类型

185 阅读6分钟

TypeScript 类型系统示意图

在 TypeScript 的类型系统中,never 类型常常被忽略或误解,但它实际上是一个强大而精妙的设计。作为类型空间的底层类型,never 代表的是"永远不会发生"的值,它在类型安全和高级类型操作中扮演着关键角色。本文将全面剖析 never 类型的本质和应用场景。

什么是 Never 类型?

基本定义

never 类型表示的是那些永远不可能存在的值的类型。更准确地说:

  • 它是任何类型都无法赋值给的底层类型
  • 它表示函数永远不会正常返回(抛出异常或陷入死循环)
  • 在联合类型中,never 类型会自动消失
  • 在交叉类型中,never覆盖所有其他类型
// never 类型的基本特征
type Example = never; // 无法被赋任何值

// 所有类型都不能赋值给 never
let impossible: never;
impossible = "test"; // ❌ 错误:不能将类型"string"分配给类型"never"

never vs void vs unknown

特性nevervoidunknown
值分配无值可分配只能分配 undefined/null所有值可分配
函数返回值永不返回的函数无返回值的函数不应用于函数返回值
类型收窄表示不可能情况无特殊作用需要类型收窄
类型操作中从联合类型消失保留在联合类型中在联合类型中覆盖其他类型
赋值给其他可赋值给任何类型需要配置严格空值检查不能直接赋值给其他类型

为什么需要 Never 类型?

1. 表示不可能发生的情况

never 类型用于标记在程序控制流中不应该到达的代码路径。这有助于捕获逻辑错误:

type Shape = "circle" | "square" | "triangle";

function calculateArea(shape: Shape): number {
    switch(shape) {
        case "circle":
            return Math.PI * radius ** 2;
        case "square":
            return sideLength ** 2;
        default:
            // 此时,类型已被收窄为 'never'
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

在上述代码中,如果未来添加了新形状但忘记更新 calculateArea 函数:

// 新增 "pentagon" 形状
type Shape = "circle" | "square" | "triangle" | "pentagon";

function calculateArea(shape: Shape): number {
    switch(shape) {
        // ...原有实现
        default:
            const _exhaustiveCheck: never = shape; // ❌ 错误!
            // Type '"pentagon"' is not assignable to type 'never'
            return _exhaustiveCheck;
    }
}

编译器会立即提醒我们处理新增的形状,这是强大的穷尽性检查机制

2. 标识特殊函数类型

当函数永远不会正常返回时(抛出异常或无限循环),使用 never 作为返回类型:

// 抛出错误的函数
function panic(message: string): never {
    throw new Error(message);
}

// 无限循环函数
function eternalLoop(): never {
    while(true) {
        // 永远执行
    }
}

// 使用场景:关键操作前验证
function saveUser(user: User) {
    if (!user.isValid()) {
        panic("Invalid user cannot be saved!");
    }
    // 验证通过后的保存逻辑
}

Never 类型的高级应用

1. 类型空间中的空集

在类型系统中,never 是空集,因此具有以下数学属性:

type T1 = string & never;    // never (任何类型与 never 交叉 = never)
type T2 = string | never;    // string (联合类型中 never 消失)

type T3 = keyof never;       // never 
type T4 = never[any];        // never

2. 在条件类型中的过滤

never 在条件类型的分发机制中会过滤掉不匹配的类型

type Filter<T, U> = T extends U ? T : never;

type Numbers = Filter<string | number | boolean, number>; 
// 结果:number

// 实际应用:从对象中提取函数属性
type FunctionKeys<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];

type User = {
    id: number;
    name: string;
    save: () => void;
};

type UserFunctionKeys = FunctionKeys<User>; // "save"

3. 类型空间中的防御性编程

使用 never 阻止不可能的分支:

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

type Direction = "up" | "down";

function move(dir: Direction) {
    switch(dir) {
        case "up": 
            return "Moving up";
        case "down":
            return "Moving down";
        default:
            return assertNever(dir); // 安全防护
    }
}

4. Tuple 中的占位符

在元组类型中使用 never 来禁止特定索引位置的赋值:

type SafeTuple = [string, never, number];

const tuple: SafeTuple = ["first", /* 不能赋值 */, 3]; 
// ✅ 合法,第二个位置保持未初始化

// 尝试赋值会导致错误
tuple[1] = "something"; // ❌ 错误:不能将类型"string"分配给类型"never"

实际应用场景

1. Redux Action 处理

在 Redux reducer 中使用 never 进行穷尽性检查,确保处理所有 action:

type Actions = 
  | { type: 'ADD_TODO'; text: string }
  | { type: 'DELETE_TODO'; id: number }
  | { type: 'TOGGLE_TODO'; id: number };

function todoReducer(state: Todo[], action: Actions) {
  switch (action.type) {
    case 'ADD_TODO': 
      // 处理逻辑
      break;
    case 'DELETE_TODO':
      // 处理逻辑
      break;
    case 'TOGGLE_TODO':
      // 处理逻辑
      break;
    default:
      const exhaustiveCheck: never = action;
      return exhaustiveCheck;
  }
}

// 当添加新 action 但未更新 reducer 时:
// { type: 'EDIT_TODO'; id: number; text: string } -> ❌ 类型错误

2. 高级类型工具库

在类型工具中使用 never 构建复杂类型:

// 从 T 中排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// 获取函数参数类型
type Parameters<T extends (...args: any) => any> = 
  T extends (...args: infer P) => any ? P : never;

// 获取 Promise 的解决值类型
type UnpackPromise<T> = 
  T extends Promise<infer U> ? U : never;

3. 防御性 API 设计

在 API 边界使用 never 强制要求编译时检查:

interface ApiResponse<T, E = never> {
    data: T;
    error?: E;
}

// 成功的响应:没有 error 类型
type SuccessResponse = ApiResponse<{ id: number }>;

// 错误响应:明确 error 类型
type ErrorResponse = ApiResponse<number, { code: number; message: string }>;

// 使用场景
function handleResponse(response: ApiResponse<any>) {
    if (response.error) {
        // 类型系统知道这里有 error 对象
        console.error(response.error.message);
    } else {
        // 类型系统保证这里的 data 安全
        console.log("Data:", response.data);
    }
}

Never 类型的陷阱与误区

  1. 误用作为变量类型

    let mistake: never = 1; // ❌ 永远无法赋值
    
  2. 混淆 nevervoid

    function example(): void {
      throw new Error(); // 正确做法应是返回 never
    }
    
  3. 过度复杂的类型操作

    // 过于复杂的类型操作可能产生未预期的 never
    type Confusing = { [K: string]: never } & string;
    

最佳实践原则

  1. 使用 never 进行穷尽性检查:在 switch/case 或 if/else 链的 default 分支中用 never 捕获未处理的情况

  2. 为永不返回的函数指定返回类型:明确标记 panic()infiniteLoop() 等函数返回 never

  3. 在类型操作中利用 never 的过滤特性:使用 never 在条件类型中排除不想要的类型

  4. 避免直接使用 never 定义变量:因为没有任何值可以赋值给 never 类型的变量

  5. 与类型断言配合使用

    function parseJSON(text: string): unknown {
      try {
          return JSON.parse(text);
      } catch (e) {
          // 错误情况下不返回任何值
          return undefined as never;
      }
    }
    

掌握类型系统的底层基础

never 类型是 TypeScript 类型系统中的基础构建块,理解和熟练运用它可以带来以下收益:

  • 🔍 更强的类型安全:通过穷尽性检查捕获未处理的情况
  • 🛠️ 更精确的类型操作:在条件类型和映射类型中过滤无效状态
  • 更清晰的代码意图:明确标记永不返回的函数
  • 🧠 更深层的类型系统理解:掌握类型空间的数学基础

在实际项目中,合理利用 never 类型就像是拥有了一个编译时的逻辑检查器,它能在代码运行前捕获许多潜在错误。尝试在下一次编写复杂类型逻辑时思考:"这里可以使用 never 来保证类型安全吗?"这可能会是你掌握 TypeScript 高级类型系统的关键一步。

"在类型系统的宇宙中,never 是奇点——一个容纳一切又排斥一切的点。" —— TypeScript 类型哲学