enum 深挖

2026-09-09 00:00    #typescript  

enum 是 TypeScript 独有的语法,它有三种形态、会生成运行时产物、类型安全性还有缺口。这一篇讲清楚它的实际行为,以及为什么新代码通常不该用 enum。

三种形态

 1// enum 的三种形态
 2
 3// 1. 数字枚举:默认从 0 递增
 4enum Status {
 5  Idle,      // 0
 6  Loading,   // 1
 7  Done,      // 2
 8}
 9
10// 手动指定起始值
11enum Code {
12  Ok = 200,
13  NotFound = 404,
14  Error = 500,
15}
16
17// 中间指定:后面的跟着递增
18enum Mixed {
19  A,          // 0
20  B = 10,     // 10
21  C,          // 11
22  D,          // 12
23}
24
25// 2. 字符串枚举:必须每个都赋值
26enum Direction {
27  Up = "UP",
28  Down = "DOWN",
29}
30
31// 3. 异构枚举:数字和字符串混合(不推荐)
32enum Mixed2 {
33  No = 0,
34  Yes = "YES",
35}
36
37// const enum:编译时内联,不生成运行时对象
38const enum Fast {
39  A = 1,
40  B = 2,
41}
42
43// 反向映射:只有数字枚举有
44const a = Status.Idle;      // 0
45const b = Status[0];        // "Idle"  ← 反向映射
46// 字符串枚举没有反向映射
47// const c = Direction["UP"];   // 报错
48
49// const enum 的用法:值被内联
50const fastValue = Fast.A;   // 编译后直接是 1
51
52// 枚举成员作为类型
53type S = Status.Done;       // 类型是 Status.Done(字面量枚举成员)
54const s: S = Status.Done;
55// @ts-expect-error TS2322: 不能赋其他成员
56const s2: S = Status.Idle;
57
58// 枚举作为联合使用
59function handle(status: Status): string {
60  switch (status) {
61    case Status.Idle: return "idle";
62    case Status.Loading: return "loading";
63    case Status.Done: return "done";
64  }
65}
66
67// 数字枚举的类型安全性较弱:
68// - 任意 number 变量可以赋给它(这是主要问题)
69// - 但不是任意数字字面量
70const anyNumber: number = 1;
71const fromNumber: Status = anyNumber;      // 允许,失去检查
72// @ts-expect-error TS2322: 999 不是 Status 的成员
73const fromLiteral: Status = 999;
74
75// 枚举可以赋给 number
76const asNumber: number = Status.Idle;
77
78console.log(a, b, Direction.Up, Mixed2.Yes, fastValue, s, handle(Status.Done));
79console.log(Code.Ok, Mixed.C, fromNumber, asNumber, fromLiteral);
10 Idle UP YES 1 2 done
2200 11 1 0 999
形态写法反向映射
数字枚举enum E { A, B }✓ 有
字符串枚举enum E { A = "a" }✗ 无
异构枚举enum E { A = 0, B = "b" }部分
const enumconst enum E { A = 1 }编译时内联

数字枚举默认从 0 递增,也可以指定起始值:

1enum Code { Ok = 200, NotFound = 404, Error = 500 }
2enum Mixed { A, B = 10, C, D }    // 0, 10, 11, 12(中间指定后续跟着递增)

字符串枚举必须每个成员都赋值——这是好事,因为字符串枚举没有反向映射,也就没有数字枚举那些问题。

反向映射只存在于数字枚举

1enum Status { Idle, Loading, Done }
2
3Status.Idle;      // 0
4Status[0];        // "Idle"  ← 反向映射

字符串枚举做不到:

1enum Direction { Up = "UP" }
2Direction.Up;       // "UP"
3Direction["UP"];    // 报错:没有反向映射

编译产物:不可擦除语法

这是 enum 最重要的性质——它留下运行时代码(见 类型擦除)。

数字枚举编译成一个 IIFE 加双向映射对象:

1var Status;
2(function (Status) {
3    Status[Status["Idle"] = 0] = "Idle";
4    Status[Status["Loading"] = 1] = "Loading";
5})(Status || (Status = {}));

字符串枚举只有单向赋值:

1var Direction;
2(function (Direction) {
3    Direction["Up"] = "UP";
4    Direction["Down"] = "DOWN";
5})(Direction || (Direction = {}));

const enum 则是编译时内联,默认不生成对象:

1const enum Fast { A = 1, B = 2 }
2const x = Fast.A;

编译后:

1const x = 1 /* Fast.A */;

值被直接替换成 1,Fast 对象不存在。

preserveConstEnums 编译选项

开启 preserveConstEnums 后,const enum 的定义会被保留:

1var Fast;
2(function (Fast) {
3    Fast[Fast["A"] = 1] = "A";
4    Fast[Fast["B"] = 2] = "B";
5})(Fast || (Fast = {}));
6const x = 1 /* Fast.A */;
注意它没有改变使用点

preserveConstEnums 只是保留枚举对象本身(这样运行时反射能拿到它),使用点仍然被内联为 1 /* Fast.A */。它不等于"把 const enum 变成普通 enum"。

用途是:当你的代码被别的工具(如 Babel 单独转译)处理,或者需要在运行时遍历枚举成员时,保留定义才有意义。

Node 原生跑不了 enum

因为 enum 不可擦除,Node 的 type stripping 直接拒绝:

1$ node file-with-enum.ts
2SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
3    code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'

const enum 同样报错。要跑就得用 tsc 编译或 tsx。详见 运行 TypeScript。

类型安全性的缺口

数字枚举最被诟病的问题:任意 number 变量都能赋给它。

1enum Status { Idle, Loading }
2
3const n: number = 1;
4const s: Status = n;        // 允许!失去了所有检查

但规则比"接受任意数字"更细——实测确认:

来源结果
const n: number = 1; const s: Status = n;✓ 允许
const s: Status = 999;✗ TS2322
Status.Idle 赋给 number✓ 允许

所以准确说法是:枚举对 number 是"双向宽松"的——number 变量能进来,枚举值能出去,但枚举外的数字字面量进不来。这个不对称规则在实践中还是会漏掉错误(比如把 HTTP 状态码变量赋给 Code 枚举)。

枚举成员本身作为类型是严格的:

1type S = Status.Done;
2const s2: S = Status.Idle;
3// error TS2322: Type 'Status.Idle' is not assignable to type 'Status.Done'.

为什么不推荐用 enum

 1// 为什么不推荐用 enum:四种替代方案
 2
 3// ---- 问题 1:数字枚举失去类型检查 ----
 4enum Status {
 5  Idle,
 6  Loading,
 7}
 8const n: number = 1;
 9const s: Status = n;        // 允许!任意 number 都能赋给数字枚举
10
11// ---- 问题 2:enum 是不可擦除语法,Node 原生跑不了 ----
12// 见 non-erasable 说明:node 直接跑含 enum 的文件会报
13// ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
14
15// ---- 问题 3:运行时产物有成本 ----
16// 每个非 const enum 都生成一个 IIFE + 双向映射对象
17
18// ============ 替代方案 1:字面量联合(最推荐)============
19type Status2 = "idle" | "loading" | "done";
20const s2: Status2 = "idle";
21// @ts-expect-error TS2322: 不在联合里
22const bad2: Status2 = "unknown";
23// 优点:可擦除、可序列化、类型严格、没有运行时产物
24
25// ============ 替代方案 2:as const 对象 ============
26const STATUS = {
27  Idle: "idle",
28  Loading: "loading",
29  Done: "done",
30} as const;
31
32type Status3 = (typeof STATUS)[keyof typeof STATUS];   // "idle" | "loading" | "done"
33const s3: Status3 = STATUS.Idle;
34// 优点:既有命名空间式的访问(STATUS.Idle),又有联合类型的严格性
35
36// ============ 替代方案 3:declare 对象 + 同名类型 ============
37const STATUS4 = {
38  Idle: 0,
39  Loading: 1,
40} as const;
41
42type Status4 = (typeof STATUS4)[keyof typeof STATUS4];   // 0 | 1
43const s4: Status4 = STATUS4.Idle;
44// @ts-expect-error TS2322: 2 不是 0 | 1
45const bad4: Status4 = 2;
46
47// ============ 替代方案 4:真的需要数字枚举时 ============
48// 如果必须和数字打交道(如协议字段),用 as const + 显式联合
49const CODES = [200, 404, 500] as const;
50type Code = (typeof CODES)[number];    // 200 | 404 | 500
51const c: Code = 200;
52// @ts-expect-error TS2322: 201 不在联合里
53const badC: Code = 201;
54
55// ---- 什么时候 enum 仍然合理 ----
56// 1. 已有大量代码在用(迁移成本 > 收益)
57// 2. 需要反向映射(数字 → 名字),且确实用得到
58// 3. 项目必须用 tsc 编译(不能改用 tsx/Node 原生)
59
60function describe(s: Status2): string {
61  switch (s) {
62    case "idle": return "空闲";
63    case "loading": return "加载中";
64    case "done": return "完成";
65  }
66}
67
68console.log(s, s2, s3, s4, c, describe("idle"), STATUS.Done, STATUS4.Loading, bad2, bad4, badC);
11 idle idle 0 200 空闲 done 1 unknown 2 201

三个问题:

  1. 类型安全性有缺口:任意 number 可以赋给数字枚举
  2. 不可擦除:Node 原生跑不了,必须走编译
  3. 有运行时成本:每个非 const enum 都生成 IIFE 和映射对象

四种替代方案

方案一:字面量联合(最推荐)

1type Status = "idle" | "loading" | "done";

可擦除、可序列化、类型严格、零运行时产物。

方案二:as const 对象

1const STATUS = {
2  Idle: "idle",
3  Loading: "loading",
4  Done: "done",
5} as const;
6
7type Status = (typeof STATUS)[keyof typeof STATUS];   // "idle" | "loading" | "done"

保留了 STATUS.Idle 这种命名空间式访问,同时有联合类型的严格性。这是替代 enum 的最佳选择。

方案三:数字常量对象

需要数字值时:

1const STATUS = { Idle: 0, Loading: 1 } as const;
2type Status = (typeof STATUS)[keyof typeof STATUS];   // 0 | 1
3
4const s: Status = STATUS.Idle;
5const bad: Status = 2;
6// error TS2322: Type '2' is not assignable to type '0 | 1'.

注意这里 2 真的被拒绝了——这正是数字枚举做不到的。

方案四:从数组推导

1const CODES = [200, 404, 500] as const;
2type Code = (typeof CODES)[number];    // 200 | 404 | 500

单一数据源,还能运行时遍历。

什么时候 enum 仍然合理

迁移建议

老项目里 enum 不必急着改。但新代码建议用 as const 对象——它同时解决了类型安全、可擦除、零运行时三个问题,而且迁移路径很自然:

1// 改前
2enum Status { Idle = "idle", Loading = "loading" }
3// 改后
4const Status = { Idle: "idle", Loading: "loading" } as const;
5type Status = (typeof Status)[keyof typeof Status];

使用点 Status.Idle 完全不用改。

相关