一句话概括
TypeScript 枚举(enum)在编译后会变成双向映射的 JavaScript 对象,而 const enum 会在编译时直接内联替换——理解两者的编译差异是正确使用枚举的关键。
背景
枚举是 TypeScript 中少数几个运行时存在的特性之一(大多数 TS 特性都是纯编译时)。这意味着:
- 枚举会影响生成的 JavaScript 代码体积
- 枚举的编译方式决定了它在不同场景下的适用性
- 不理解编译结果,就无法做出正确的枚举选型
面试中,枚举相关问题是”TypeScript 深度”的试金石:
- “enum 和 const enum 有什么区别?” —— 高频题
- “枚举编译后长什么样?” —— 进阶题
- “什么时候用枚举,什么时候用联合类型?” —— 场景题
概念与定义
什么是枚举?
枚举(Enum)是 TypeScript 中用于定义一组命名常量的语法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 数字枚举
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 字符串枚举
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
// 异构枚举(不推荐)
enum Mixed {
No = 0,
Yes = "YES",
}
|
枚举的核心价值
- 语义化:用命名常量替代魔法数字/字符串
- 类型安全:限制变量只能取枚举成员的值
- 可维护性:修改枚举值只需改一处
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 定义枚举
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalServerError = 500,
}
// 使用枚举
function handleError(status: HttpStatus): string {
switch (status) {
case HttpStatus.OK:
return "请求成功";
case HttpStatus.NotFound:
return "资源未找到";
case HttpStatus.InternalServerError:
return "服务器错误";
default:
return "未知状态";
}
}
handleError(HttpStatus.OK); // "请求成功"
handleError(200); // "请求成功"(数字枚举允许反向映射)
// handleError(201); // ❌ 不是 HttpStatus 的成员(但数字枚举有特例)
|
核心知识点拆解
1. 数字枚举的编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // TypeScript 源码
enum Direction {
Up,
Down,
Left,
Right,
}
// 编译后的 JavaScript
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up"; // Direction.Up = 0; Direction[0] = "Up"
Direction[Direction["Down"] = 1] = "Down"; // Direction.Down = 1; Direction[1] = "Down"
Direction[Direction["Left"] = 2] = "Left"; // Direction.Left = 2; Direction[2] = "Left"
Direction[Direction["Right"] = 3] = "Right"; // Direction.Right = 3; Direction[3] = "Right"
})(Direction || (Direction = {}));
|
关键发现:数字枚举生成了双向映射对象
1
2
3
4
5
| // 运行时对象结构
console.log(Direction.Up); // 0(正向映射:名 → 值)
console.log(Direction[0]); // "Up"(反向映射:值 → 名)
console.log(Direction); // { 0: "Up", 1: "Down", 2: "Left", 3: "Right",
// Up: 0, Down: 1, Left: 2, Right: 3 }
|
2. 字符串枚举的编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // TypeScript 源码
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
// 编译后的 JavaScript
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
Status["Pending"] = "PENDING";
})(Status || (Status = {}));
|
关键发现:字符串枚举没有反向映射
1
2
3
| console.log(Status.Active); // "ACTIVE"(正向映射:名 → 值)
console.log(Status["ACTIVE"]); // undefined(没有反向映射!)
console.log(Status); // { Active: "ACTIVE", Inactive: "INACTIVE", Pending: "PENDING" }
|
3. const enum 的编译结果
1
2
3
4
5
6
7
8
9
10
11
12
| // TypeScript 源码
const enum Color {
Red,
Green,
Blue,
}
const color = Color.Red;
// 编译后的 JavaScript(isolatedModules: false)
var color = 0 /* Color.Red */;
// 注意:没有生成 Color 对象!值被直接内联替换
|
1
2
3
4
5
6
7
8
9
10
11
12
| // const enum 的访问方式
const enum HttpStatus {
OK = 200,
NotFound = 404,
}
const status: HttpStatus = HttpStatus.OK;
// 编译后:var status = 200;
// ❌ const enum 不支持运行时访问
// console.log(HttpStatus); // 编译错误
// const keys = Object.keys(HttpStatus); // 编译错误
|
4. const enum 的陷阱:isolatedModules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 当 tsconfig.json 中 isolatedModules: true 时
const enum Color {
Red = 0,
}
// ❌ 直接使用 const enum 成员会报错
const c = Color.Red;
// Error: 'const' enums can only be used with property access
// using the enum object or direct value
// ✅ 解决方案1:使用索引访问
const c = Color["Red"]; // 在 isolatedModules 模式下安全
// ✅ 解决方案2:改用普通 enum
enum Color {
Red = 0,
}
// ✅ 解决方案3:使用 as const 对象 + 类型
const Color = {
Red: 0,
Green: 1,
Blue: 2,
} as const;
type Color = typeof Color[keyof typeof Color];
|
5. 枚举成员的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 枚举成员本身也是类型
enum Shape {
Circle,
Square,
Triangle,
}
// 枚举成员类型:只能取特定值
let shape: Shape.Circle = Shape.Circle;
// shape = Shape.Square; // ❌ 不能赋值为其他枚举成员
// 枚举类型:可以取任意成员值
let anyShape: Shape = Shape.Circle;
anyShape = Shape.Square; // ✅
anyShape = Shape.Triangle; // ✅
|
实战案例
案例一:HTTP 状态码枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // 数字枚举:适合状态码场景
enum HttpStatusCode {
// 2xx 成功
OK = 200,
Created = 201,
NoContent = 204,
// 3xx 重定向
MovedPermanently = 301,
Found = 302,
// 4xx 客户端错误
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
// 5xx 服务器错误
InternalServerError = 500,
BadGateway = 502,
ServiceUnavailable = 503,
}
// 利用反向映射获取状态码描述
function getStatusText(code: HttpStatusCode): string {
return HttpStatusCode[code]; // 数字枚举支持反向映射
}
console.log(getStatusText(404)); // "NotFound"
console.log(HttpStatusCode[200]); // "OK"
// 类型安全的响应处理
function handleResponse(status: HttpStatusCode): void {
if (status >= 200 && status < 300) {
console.log("成功响应");
} else if (status >= 400 && status < 500) {
console.log("客户端错误");
} else if (status >= 500) {
console.log("服务器错误");
}
}
|
案例二:使用 as const 对象替代枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 很多团队更倾向于用 as const 对象 + 联合类型替代枚举
const UserRole = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER",
} as const;
// 类型定义
type UserRole = typeof UserRole[keyof typeof UserRole];
// 等价于: "ADMIN" | "EDITOR" | "VIEWER"
// 使用
function canEdit(role: UserRole): boolean {
return role === UserRole.Admin || role === UserRole.Editor;
}
canEdit("ADMIN"); // ✅
canEdit(UserRole.Admin); // ✅
// canEdit("SUPER"); // ❌ 类型错误
// 优势对比
// as const 对象:tree-shakeable、没有编译产物、与 JS 生态兼容
// enum:反向映射、类型更严格、编译产物更多
|
案例三:枚举与映射表结合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 枚举 + 映射表:类型安全的配置
enum FeatureFlag {
DarkMode = "DARK_MODE",
NewDashboard = "NEW_DASHBOARD",
BetaAPI = "BETA_API",
}
const featureConfig: Record<FeatureFlag, { enabled: boolean; description: string }> = {
[FeatureFlag.DarkMode]: { enabled: true, description: "暗黑模式" },
[FeatureFlag.NewDashboard]: { enabled: false, description: "新仪表盘" },
[FeatureFlag.BetaAPI]: { enabled: false, description: "Beta API" },
};
function isFeatureEnabled(flag: FeatureFlag): boolean {
return featureConfig[flag].enabled;
}
// 新增枚举成员时,映射表会自动报错提醒
// 如果在 FeatureFlag 中新增了成员但忘记在 featureConfig 中添加,
// TypeScript 会立即报错:类型缺少属性
|
底层原理
枚举编译的本质
1
2
3
4
5
| // 数字枚举的编译核心逻辑
Direction[Direction["Up"] = 0] = "Up";
// 等价于两步操作:
// 1. Direction["Up"] = 0 → 设置正向映射
// 2. Direction[0] = "Up" → 设置反向映射
|
为什么数字枚举有反向映射?
- 数字值本身没有语义信息(0 不能告诉你它代表”上”)
- 反向映射允许通过值查找名称,便于调试和日志
- 字符串枚举的值本身就有语义(”ACTIVE” 已经够清楚),不需要反向映射
const enum 内联的原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 编译器在编译时就能确定 const enum 的值
const enum Bit {
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
ReadWrite = Read | Write, // 3
}
const perm: Bit = Bit.ReadWrite;
// 编译器在编译阶段计算出 Read | Write = 3
// 直接替换为: var perm = 3;
// 位运算枚举是 const enum 的经典场景
// 因为值在编译时确定,运行时不需要对象查找
|
枚举的类型系统实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // TypeScript 内部如何处理枚举类型
enum Days {
Mon,
Tue,
Wed,
}
// 枚举类型在类型系统中的结构
// Days → 枚举类型(值类型)
// Days.Mon → 枚举成员类型(更窄的类型)
// 数字枚举的特殊规则:任意数字都可以赋值给数字枚举类型
let day: Days = Days.Mon;
day = 100; // ✅ 数字枚举的特殊行为(历史遗留设计)
// 这是因为数字枚举的值可能是动态计算的
// 字符串枚举没有这个行为
enum Status { Active = "A", Inactive = "I" }
let status: Status = Status.Active;
// status = "X"; // ❌ 字符串枚举不允许
|
💡 人话总结:
- 数字枚举编译成双向映射对象(名↔值),字符串枚举编译成单向映射(名→值)
- const enum 编译成直接值,不生成任何对象——最轻量但受限最多
- 如果不需要反向映射,优先用 as const 对象替代枚举
高频面试题解析
Q1:enum 和 const enum 有什么区别?
参考答案:
| 特性 | enum | const enum |
|---|
| 编译产物 | 生成 JavaScript 对象 | 直接内联替换,无运行时代码 |
| 反向映射 | 数字枚举支持 | 不支持(没有运行时对象) |
| 运行时访问 | 可以(Object.keys 等) | 不可以 |
| tree-shaking | 无法被 tree-shake | 天然 tree-shakeable |
| isolatedModules | 兼容 | 有陷阱(需注意用法) |
| 计算成员 | 支持 | 不支持(必须是常量表达式) |
1
2
3
4
5
6
7
8
9
| // enum:运行时存在
enum Color { Red, Green, Blue }
console.log(Color); // { 0: "Red", 1: "Green", 2: "Blue", Red: 0, Green: 1, Blue: 2 }
console.log(Color[0]); // "Red"(反向映射)
// const enum:编译时替换
const enum ConstColor { Red, Green, Blue }
const c = ConstColor.Red; // 编译为: var c = 0;
// console.log(ConstColor); // ❌ 编译错误
|
Q2:数字枚举和字符串枚举有什么区别?
参考答案:
| 特性 | 数字枚举 | 字符串枚举 |
|---|
| 默认值 | 自增数字(0, 1, 2…) | 必须显式赋值 |
| 反向映射 | ✅ 支持 | ❌ 不支持 |
| 赋值灵活性 | 可接受任意数字 | 只能接受枚举成员 |
| 序列化 | 数字无语义 | 字符串有语义 |
1
2
3
4
5
6
7
8
9
| enum Numeric { A, B, C }
enum StringBased { A = "a", B = "b", C = "c" }
// 数字枚举:可接受任意数字(历史设计)
let n: Numeric = 999; // ✅
// 字符串枚举:严格限制
let s: StringBased = StringBased.A; // ✅
// let s2: StringBased = "d"; // ❌
|
推荐:优先使用字符串枚举——值有语义、类型更严格、调试更友好。
Q3:什么时候用枚举,什么时候用联合类型?
参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 场景1:值已知且固定 → 联合类型更轻量
type Status = "active" | "inactive" | "pending";
// 无编译产物,零运行时开销
// 场景2:需要反向映射或运行时遍历 → 枚举
enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}
Object.values(Status); // ["active", "inactive", "pending"]
// 场景3:需要语义化 + 零运行时 → as const 对象
const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending",
} as const;
type Status = typeof Status[keyof typeof Status];
|
选择建议:
- 简单常量集合 → 联合类型
- 需要运行时访问 → as const 对象
- 需要反向映射 → 数字枚举
- 需要严格枚举语义 → 字符串枚举
Q4:const enum 在 isolatedModules 模式下有什么问题?
参考答案: 当 tsconfig.json 设置 isolatedModules: true(Babel、esbuild、swc 等使用)时,const enum 的行为受限:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const enum Foo {
A = 1,
}
// ❌ 直接使用成员可能在不同编译器下行为不一致
export const x = Foo.A;
// ✅ 索引访问方式安全
export const y = Foo["A"];
// 根本原因:
// isolatedModules 模式下,每个文件独立编译
// const enum 的内联替换需要跨文件信息
// Babel/esbuild 等工具无法处理跨文件的内联
|
解决方案:
- 禁用
isolatedModules(不推荐,影响构建工具选择) - 不使用 const enum,改用普通 enum 或 as const 对象
- 只使用索引访问方式
Foo["A"]
总结与扩展
核心要点
- 数字枚举:编译为双向映射对象,支持反向映射
- 字符串枚举:编译为单向映射,类型更严格
- const enum:编译为直接值,零运行时开销,但受限于 isolatedModules
- as const 对象:枚举的现代替代方案,兼容性更好
- 选型依据:根据反向映射需求、运行时访问需求、tree-shaking 需求做选择
延伸学习方向
- 联合类型与字面量类型:枚举的轻量替代方案
- as const 断言:TypeScript 3.4+ 的常量类型推断
- 模板字面量类型:与字符串枚举结合的高级用法
- namespace 与枚举:枚举与命名空间合并的模式
相关主题