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 enum | const 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
三个问题:
- 类型安全性有缺口:任意
number可以赋给数字枚举 - 不可擦除:Node 原生跑不了,必须走编译
- 有运行时成本:每个非
constenum 都生成 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 仍然合理
- 已有大量代码在用:迁移成本可能大于收益
- 真的需要反向映射:数字 → 名字,且确实用得到
- 项目必须用
tsc编译:不能改用 Node 原生或 esbuild 直接跑
老项目里 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 完全不用改。
相关
- 类型擦除 —— 为什么 enum 会留下运行时产物
- 运行 TypeScript —— Node 原生 type stripping 的限制
- satisfies 与 as const ——
as const的完整用法 - 类型收窄 —— 字面量联合的穷尽性检查