文章

枚举的本质与编译结果深度解析

enum 是 TypeScript 少数'活到运行时'的语法:数字枚举编译出双向映射对象,const enum 直接蒸发。 选错要么包体积多几 KB,要么 isolatedModules 直接报错。

枚举的本质与编译结果深度解析

一句话概括

enum 是 TypeScript 中少数几个”活到运行时”的语法——数字枚举编译出双向映射对象,const enum 直接蒸发,选错了要么包体积多几 KB,要么 isolatedModules 直接报错。

核心知识点

1. 数字枚举 → 双向映射对象(有代价)

1
2
3
4
5
6
7
8
9
10
11
12
enum Direction { Up, Down, Left, Right }

// 编译后的 JS(简化):
// var Direction;
// (function (Direction) {
//   Direction[Direction["Up"] = 0] = "Up";
//   Direction[Direction["Down"] = 1] = "Down";
//   // ... 双向映射:值 → 名,名 → 值
// })(Direction || (Direction = {}));

console.log(Direction.Up);    // 0(名 → 值)
console.log(Direction[0]);    // "Up"(值 → 名,反向映射)

反向映射的代价:这段 IIFE 有副作用,打包工具无法 tree-shake——即使你只用了 Direction.Up,整个枚举对象都会打进 bundle。

2. 字符串枚举 → 单向映射,更干净

1
2
3
4
5
6
7
8
9
10
11
12
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

// 编译后:{ Active: "ACTIVE", Inactive: "INACTIVE" }
// 没有反向映射,但没有 IIFE 包裹,也是无法 tree-shake 的对象

// 类型比数字枚举严格:
let s: Status = Status.Active; // ✅
// s = "ACTIVE";               // ❌ 不能把字面量赋给枚举类型
// s = "DELETED";              // ❌ 不在枚举定义中

3. const enum → 编译时内联,零运行时

1
2
3
const enum Color { Red, Green, Blue }
const c = Color.Red;
// 编译后:const c = 0; ← Color 对象完全消失,直接内联为字面量!

优点:零运行时开销,天然 tree-shakeable。
致命缺点:isolatedModules: true(几乎所有现代项目都开)下可能报错——Babel/esbuild/swc 等非 tsc 编译器无法跨文件内联 const enum。

1
2
3
4
5
6
// a.ts
export const enum Status { Active, Inactive }

// b.ts — swc/esbuild 编译时:
import { Status } from './a';
const s = Status.Active; // ❌ 可能报错:无法解析 const enum

4. 数字枚举的「不安全」设计

1
2
3
4
5
6
7
enum Status { Active, Inactive }
let s: Status = 999; // ✅ 不报错!数字枚举接受任意 number 值
// 这是历史设计缺陷——TS 为了兼容 JS 的位运算场景做的妥协

// 字符串枚举没有此问题:
enum StringStatus { Active = "A", Inactive = "I" }
// let ss: StringStatus = "X"; // ❌ 报错

5. as const 对象:TS 4.x 时代的最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 推荐:用 as const 对象替代枚举
const UserRole = {
  Admin: "ADMIN",
  Editor: "EDITOR",
  Viewer: "VIEWER",
} as const;

// 值和类型从一个源头出,新增角色只改一处
type UserRole = (typeof UserRole)[keyof typeof UserRole];
// "ADMIN" | "EDITOR" | "VIEWER"

// 优势一览:
// ✅ 零运行时开销——就是普通对象,天然 tree-shakeable
// ✅ isolatedModules 友好——不需要跨文件编译感知
// ✅ 可遍历——Object.keys/values/entries 随便用
// ✅ 类型同源——新增值自动加入联合类型

其实你每天都在用

  1. HTTP 状态码映射:HttpCode[404] → "NotFound" 打日志时反向查可读语义
  2. Redux Action Type:const enum ActionType { FETCH_USER = "FETCH_USER" }——能用 const enum 的场景之一,action type 不会跨包引用
  3. 位运算权限:const enum Perm { Read = 1, Write = 2, Admin = 4 } → 编译后就是纯数字 | 运算,零开销
  4. 组件尺寸/颜色 prop:type Size = "sm" | "md" | "lg" —— 直接用联合类型,不需要 enum
  5. 环境配置常量:const ENV = { DEV: "dev", STAGING: "staging", PROD: "prod" } as const → 用 ENV.DEV 而非魔法字符串

常见误解(FAQ)

❌ 误区 1:「const enum 总是优于 enum,能省体积」

const enum 在 isolatedModules: true 下直接报错。现在几乎所有项目都用 Babel/esbuild/swc 而非 tsc 做编译——这也是为什么 create-react-app、Vite、Next.js 默认都禁用 const enum 的跨文件引用。如果项目是库(需要导出类型),const enum 基本不能用。

❌ 误区 2:「enum 跟 Java/C# 的枚举一样功能强大」

Java 的 enum 是一等公民——可以有构造器、方法、字段、实现接口。TypeScript 的 enum 只是语法糖,编译后就是普通 JS 对象,远没有 Java enum 的能力。不要按 Java 经验预设 TS enum 的能力边界。

❌ 误区 3:「联合类型能完全替代 enum」

需要反向映射(数字枚举 Direction[0] → "Up")时不能。需要运行时遍历/校验值集合时,as const 对象比联合类型更方便,但纯联合类型 "a" | "b" 没有运行时信息。选型看场景:类型约束→联合类型,运行时要用→as const。

❌ 误区 4:「枚举值应该从 0 或 1 开始」

如果你打算存数据库或跨系统传输,数字枚举的 0, 1, 2 没有任何语义——其他开发者看到 status: 2 不知道啥意思。字符串枚举或 as const 对象更合适。数字枚举最适合的场景是:位运算标记(Read | Write)和反向映射日志(Direction[0])。

一句话总结

能联合类型就联合类型,需要运行时值就用 as const 对象,真的需要反向映射再考虑 enum——别为了一个常量定义给 bundle 多塞 500 字节的 IIFE。

本文由作者按照 CC BY 4.0 进行授权