枚举
用一组命名常量,让代码自己说话
🎯 学习目标
学完本章,你将能够:
- 能够定义数字枚举、字符串枚举和 const 枚举
- 能够理解枚举的反向映射机制与编译产物
- 能够对比枚举与字面量联合类型的优劣并做出选择
- 能够使用枚举实现穷尽性检查与权限控制等实际场景
- 能够理解 const 枚举的内联优化与 isolatedModules 限制
先看一个问题
用户状态有三种:活跃、冻结、待审核。你打算怎么表示?
// 方案1:魔法数字——谁能记住 0、1、2 分别代表什么?
if (user.status === 0) { /* 活跃?冻结? */ }
// 方案2:魔法字符串——拼写错误不会报错
if (user.status === "actvie") { /* 拼错了,但不报错 */ }
// 方案3:枚举——有名字、有提示、写错会报错
enum Status { Active, Frozen, Pending }
if (user.status === Status.Active) { /* 清晰明了 */ }
枚举(Enum)就是用来解决这个问题的:把一组相关的常量放在一个名字下面,让代码既安全又好读。
什么是枚举
给一组常量起名字的容器
枚举(Enum)是 TypeScript 中一种特殊的类型,用于定义一组命名的常量集合。它可以让你用有意义的名字代替魔法数字或字符串。
打个比方:枚举就像一个下拉菜单——选项是固定的,你只能从里面选,不能随便填。
TypeScript 支持三种枚举:
数字枚举
值自动递增,支持反向映射。最基础的形式。
字符串枚举
每个成员必须显式赋值,可读性更好。
const 枚举
编译时内联,零运行时开销。
Part 1: 数字枚举
最基础的枚举形式,值从 0 开始自动递增
开篇那个 Status 枚举就是数字枚举——不赋值就自动从 0 递增。写起来简单,但它有一个独特的"副作用":反向映射。你可以用数字查到枚举名,比如 Direction[0] 得到 "Up"。我们先从基础语法开始,再揭开反向映射的原理。
数字枚举基础
自动从 0 递增,也可以自定义起始值
// 最简单的数字枚举
enum Direction {
Up, // 0(自动从 0 开始)
Down, // 1
Left, // 2
Right, // 3
}
let dir: Direction = Direction.Up;
dir = Direction.Down; // OK
// dir = 42; // 报错!
// 自定义起始值
enum Status {
Active = 1, // 1
Inactive, // 2(自动递增)
Pending, // 3
}
// 自定义所有值
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalError = 500,
}
反向映射
数字枚举的独有能力:值到名的查找
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}
let c: Color = Color.Red; // 0
let name: string = Color[0]; // "Red" —— 反向映射
let name2: string = Color[Color.Red]; // "Red"
编译后的 JavaScript 长这样——这就是反向映射能工作的原因:
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red"; // 正向 + 反向
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
反向映射是数字枚举的"副作用":编译器生成了一个双向的对象。字符串枚举和 const 枚举都没有反向映射。
Part 2: 字符串枚举
每个成员必须显式赋值,可读性和调试体验更好
数字枚举调试时看到的是 0、1、2——不查代码根本不知道什么意思。字符串枚举就没有这个问题,值本身就是可读的文本。代价是每个成员都得手动赋值。我们来看看它的语法,以及一个"混合枚举"的坑。
字符串枚举
值是字符串,没有反向映射
// 每个成员必须显式赋值
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
// 字符串枚举没有反向映射
// Direction["UP"] 不存在
// 混合枚举(不推荐)
enum Mixed {
No = 0, // 数字
Yes = "YES", // 字符串
}
// 实际应用:API 路由方法
enum Method {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
function request(url: string, method: Method) {
// method 只能是 GET/POST/PUT/DELETE
}
request("/api/users", Method.GET);
字符串枚举的编译结果更简洁——没有双向映射的代码。
异构枚举详解
数字和字符串混在一个枚举里——看似灵活,实则危险
异构枚举(Heterogeneous Enum)指的是同一个枚举里既有数字成员又有字符串成员。TypeScript 允许你这么写,但社区普遍认为这是一个设计缺陷。
// 异构枚举:数字和字符串混用
enum MixedResult {
Success = 0, // 数字
Failure = "FAIL", // 字符串
Pending = 1, // 又回到数字
}
// 编译后的 JS:混乱的双向映射
// MixedResult[0] === "Success"
// MixedResult["Failure"] === "FAIL"(没有反向映射)
// 错误做法:异构枚举
enum BadStatus {
OK = 200,
Error = "ERROR",
}
// 正确做法:拆成两个枚举,或者统一用字符串
enum HttpStatusCode {
OK = 200,
NotFound = 404,
}
enum ErrorKind {
Network = "NETWORK",
Timeout = "TIMEOUT",
}
Part 3: const 枚举
编译时直接内联,不生成 JavaScript 对象
普通枚举编译后会变成一个 JS 对象,运行时占内存。如果你只在意类型安全,不在乎运行时遍历,const 枚举是更好的选择——它会在编译时直接替换为字面量,零运行时开销。但跨模块使用时有坑,需要注意。
const 枚举
编译时替换为零开销的字面量
const enum Direction {
Up,
Down,
Left,
Right,
}
let dir = Direction.Up;
// 编译结果:let dir = 0; —— 直接替换为数字
// 字符串 const enum
const enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
let s = Status.Active;
// 编译结果:let s = "ACTIVE";
isolatedModules 模式下)。如果项目使用 Babel、esbuild 等非 TS 编译器,建议避免使用 const enum。
三种枚举对比
一张表看清差异
| 特性 | 数字枚举 | 字符串枚举 | const 枚举 |
|---|---|---|---|
| 自动赋值 | 支持(从0递增) | 必须手动赋值 | 支持 |
| 反向映射 | 支持 | 不支持 | 不支持 |
| 编译输出 | 生成 JS 对象 | 生成 JS 对象 | 内联字面量 |
| 运行时开销 | 有 | 有 | 无 |
| 跨模块安全 | 安全 | 安全 | 可能有坑 |

图:三种枚举的编译结果对比
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 三列并排:数字枚举→双向映射对象 | 字符串枚举→单向映射对象 | const枚举→内联替换 - 每列底部标注:正向+反向映射 | 仅正向映射 | 零运行时开销 - 配色建议:浅蓝背景,三列不同色系,代码等宽字体,箭头橙色,16:9
Part 4: 枚举 vs 联合
哪种方式更好?取决于场景
学完三种枚举,你可能已经在想了:直接用 "ADMIN" | "EDITOR" 这种联合类型不也能约束取值范围吗?确实如此。枚举在运行时存在、可以遍历,联合类型零开销、更简洁。到底选哪个?我们用一张对比表加上替代方案帮你做决策。
枚举 vs 字面量联合
两种方式都能约束值的范围
// 方式1:使用枚举
enum Role {
Admin = "ADMIN",
Editor = "EDITOR",
Viewer = "VIEWER",
}
function checkPermission(role: Role) { /* ... */ }
checkPermission(Role.Admin);
// 方式2:使用字面量联合类型
type RoleType = "ADMIN" | "EDITOR" | "VIEWER";
function checkPermission2(role: RoleType) { /* ... */ }
checkPermission2("ADMIN");
枚举的优点
- 运行时存在,可以遍历
- 值集中管理,修改只改一处
- 数字枚举支持反向映射
联合类型的优点
- 零运行时开销
- Tree-shaking 友好
- 不依赖编译器特殊处理

图:枚举方案选择决策树
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个从左到右的决策流程图 - 第一个判断:"需要运行时遍历?" 否→字面量联合 | 是→第二个判断 - 第二个判断:"需要反向映射?" 否→as const 对象 | 是→第三个判断 - 第三个判断:"性能敏感?" 是→const enum | 否→普通 enum - 配色建议:浅灰背景,判断节点用橙色菱形,推荐方案用绿色卡片,箭头用深灰 - 画面比例:16:9,避免:过度装饰、3D 效果
推荐替代方案
as const 对象 + typeof 模拟枚举
// 推荐:用 as const 对象 + typeof 模拟枚举
const Role = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER",
} as const;
type Role = typeof Role[keyof typeof Role];
// type Role = "ADMIN" | "EDITOR" | "VIEWER"
function checkPermission(role: Role) { /* ... */ }
checkPermission(Role.Admin); // OK
checkPermission("ADMIN"); // OK
// 还能遍历——枚举的优点它也有
Object.values(Role).forEach(role => {
console.log(role);
});
新项目优先使用 as const 对象 + 字面量联合类型代替 enum。只有在需要反向映射或与旧代码兼容时才用 enum。

图:枚举替代方案对比
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:三列横向对比表格风格示意图,对比"联合类型"、"as const 对象"、"枚举"三种方案的核心特点 - 第一列(标题"联合类型",浅蓝色底色): - 代码示例:`type Role = "ADMIN" | "EDITOR" | "VIEWER"` - 特点标签(绿色):零运行时开销、Tree-shaking 友好、最简洁 - 缺点标签(红色):不能遍历、值分散在代码中 - 适用场景:纯类型约束、不需要运行时遍历 - 第二列(标题"as const 对象",浅绿色底色): - 代码示例:`const Role = { Admin: "ADMIN" } as const` - 特点标签(绿色):可遍历、类型安全、两全其美 - 缺点标签(红色):无反向映射、需要手动维护键值 - 适用场景:需要遍历 + 类型安全(推荐首选) - 第三列(标题"enum 枚举",浅橙色底色): - 代码示例:`enum Role { Admin = "ADMIN" }` - 特点标签(绿色):运行时完整对象、数字枚举有反向映射 - 缺点标签(红色):有运行时开销、数字枚举类型不安全、编译产物较大 - 适用场景:需要反向映射、与旧代码兼容 - 底部画一条渐变色带,从左到右标注:"运行时开销递增 →" - 每列内部从上到下排列:代码示例 → 特点 → 缺点 → 适用场景 - 配色建议:浅灰背景,三列分别浅蓝/浅绿/浅橙底色,优点绿色标签,缺点红色标签,代码等宽字体 - 画面比例:16:9 - 避免:过度装饰、3D 效果、文字过小
编译后的真相
数字枚举在 JS 里长什么样?
数字枚举编译后会生成一个双向映射对象。这是它最独特的地方——也是最容易让人困惑的地方。
// TypeScript 源码
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 编译后的 JavaScript
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up"; // 0 → "Up"
Direction[Direction["Down"] = 1] = "Down"; // 1 → "Down"
Direction[Direction["Left"] = 2] = "Left"; // 2 → "Left"
Direction[Direction["Right"] = 3] = "Right"; // 3 → "Right"
})(Direction || (Direction = {}));

图:数字枚举的双向映射结构
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:中央展示编译后的 Direction 对象,左右两侧展示映射关系 - 正向映射(左→右):Direction.Up = 0、Direction.Down = 1、Direction.Left = 2、Direction.Right = 3(蓝色箭头) - 反向映射(右→左):Direction[0] = "Up"、Direction[1] = "Down"、Direction[2] = "Left"、Direction[3] = "Right"(橙色箭头) - 标注:"数字枚举编译后生成双向映射对象,字符串枚举只有正向映射" - 配色建议:浅灰背景,正向蓝色系,反向橙色系,对象用白色圆角矩形 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
每一行做了两件事:把数字赋给名称(正向),再把名称赋给数字索引(反向)。所以 Direction[0] 能拿到 "Up"。
穷尽检查
用 switch + never 确保所有枚举值都被处理
当你用 switch 处理枚举时,可以通过 never 类型来确保所有分支都被覆盖。如果将来新增了一个枚举值,TypeScript 会立刻报错提醒你。
enum Status { Pending, Active, Completed, Cancelled }
function getStatusLabel(status: Status): string {
switch (status) {
case Status.Pending: return "待处理";
case Status.Active: return "进行中";
case Status.Completed: return "已完成";
case Status.Cancelled: return "已取消";
default:
// 如果 Status 新增了成员但这里没处理,会报错!
const exhaustive: never = status;
throw new Error(`未知状态: ${exhaustive}`);
}
}
关键在 default 分支里把 status 赋值给 never 类型。如果所有枚举值都已处理,这行代码不可达,不会触发;如果有遗漏,TypeScript 就会报类型错误。这个技巧叫穷尽检查(Exhaustive Check)。
命名空间嵌套
用枚举的命名空间合并能力组织相关代码
枚举在 TypeScript 中既是类型,也是命名空间。你可以利用声明合并,给枚举附加相关的工具函数或嵌套枚举。
// 基础枚举
enum Color {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF",
}
// 利用命名空间合并,给枚举添加工具方法
namespace Color {
export function fromHex(hex: string): Color | undefined {
const entries = Object.entries(Color) as [string, string][];
for (const [name, value] of entries) {
if (value === hex) return Color[name as keyof typeof Color] as Color;
}
return undefined;
}
export function getRandom(): Color {
const values = [Color.Red, Color.Green, Color.Blue];
return values[Math.floor(Math.random() * values.length)];
}
}
Color.fromHex("#FF0000"); // Color.Red
Color.getRandom(); // 随机颜色
// 嵌套枚举:把相关的枚举组织在一起
namespace App {
export enum Status { Active, Inactive, Pending }
export enum Role { Admin, Editor, Viewer }
}
function handleUser(status: App.Status, role: App.Role) {
// 清晰的层级关系
}
handleUser(App.Status.Active, App.Role.Admin);
命名空间嵌套适合大型项目中组织相关常量。但对于中小项目,拆分成独立文件然后用模块导入通常更清晰。不要为了"好看"强行嵌套。
序列化处理
数字枚举和字符串枚举在网络传输中的差异
枚举值在网络请求中会变成什么?这是前后端对接时最容易踩坑的地方。
数字枚举
序列化为数字。可读性差,但体积小。反向映射在反序列化时有帮助。
字符串枚举
序列化为字符串。可读性好,调试方便,是 API 交互的首选。
// 安全解析 API 返回的枚举值
enum UserStatus {
Active = "ACTIVE",
Inactive = "INACTIVE",
Banned = "BANNED",
}
// 从 API 响应中安全解析
function parseUserStatus(raw: unknown): UserStatus {
if (typeof raw !== "string") {
throw new Error(`状态应为字符串,收到: ${typeof raw}`);
}
const validValues = Object.values(UserStatus) as string[];
if (!validValues.includes(raw)) {
throw new Error(`无效的用户状态: ${raw}`);
}
return raw as UserStatus;
}
// 使用示例
const response = JSON.parse('{"status": "ACTIVE"}');
const status = parseUserStatus(response.status);
// status: UserStatus.Active
何时用枚举
枚举 vs 联合类型 vs as const——怎么选?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 需要运行时遍历 | 枚举 / as const 对象 | 只有它们在 JS 中存在 |
| 需要反向映射 | 数字枚举 | 唯一的内置方案 |
| 纯类型约束 | 字面量联合类型 | 零开销,最简洁 |
| 需要遍历 + 类型安全 | as const 对象 | 两全其美 |
| 与外部 API 对接 | 字符串枚举 | 序列化友好 |
一句话总结:新项目优先用 as const 对象,老项目保持一致就行。别为了"优雅"在一个项目里混用多种方案。
satisfies 加持
satisfies + as const:比 enum 更安全的选择
satisfies 是 TypeScript 4.9 引入的操作符。它检查值是否符合类型,但不拓宽类型。配合 as const,能同时获得类型收窄和类型校验。
// 问题:as const 虽然好,但没有校验
const Role = {
Admin: "ADMIN",
Editor: "EDITOR",
// Viewer: "VIEWER" —— 忘了加,不会报错!
} as const;
// 解决:用 satisfies 先校验,再 as const 收窄
type RoleMap = Record<"Admin" | "Editor" | "Viewer", string>;
const Role = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER",
} as const satisfies RoleMap;
// 少一个属性 → 报错!值不是 string → 报错!
// 类型仍然保留字面量
type R = typeof Role.Admin; // "ADMIN"(不是 string)
这就是"既有类型校验,又保留字面量类型"的黄金组合。
迁移实战
从 enum 迁移到 as const 对象的分步指南
当你决定在新代码中统一使用 as const 对象时,老代码中的 enum 需要逐步替换。以下是安全的迁移步骤。
// ===== 第一步:识别目标枚举 =====
// 找出所有数字枚举,这些优先迁移(类型安全最弱)
enum OldStatus {
Active, // 0 —— 任何数字都能赋进来
Inactive, // 1
Pending, // 2
}
// ===== 第二步:创建等效的 as const 对象 =====
const NewStatus = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING",
} as const;
type NewStatus = typeof NewStatus[keyof typeof NewStatus];
// ===== 第三步:添加兼容层(过渡期使用)=====
function toNewStatus(old: OldStatus): NewStatus {
const map: Record<OldStatus, NewStatus> = {
[OldStatus.Active]: NewStatus.Active,
[OldStatus.Inactive]: NewStatus.Inactive,
[OldStatus.Pending]: NewStatus.Pending,
};
return map[old];
}
// ===== 第四步:逐文件替换,每次改一个文件 =====
// 旧代码 → 新代码,配合穷尽检查确保不遗漏
// ===== 第五步:删除旧枚举,清理兼容层 =====
// 所有引用都迁移完毕后,删除 OldStatus 和 toNewStatus
// 第六步:添加 satisfies 校验(可选但推荐)
type StatusMap = Record<"Active" | "Inactive" | "Pending", string>;
const NewStatus = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING",
} as const satisfies StatusMap;
// 迁移前后对比:
// Before: let s: OldStatus = 999; // 不报错!
// After: let s: NewStatus = "ABC"; // 报错!类型安全
权限实战
练习
任务:定义一个权限系统的枚举或联合类型(Read / Write / Delete / Admin),然后实现 checkPermission 函数,判断用户是否拥有某权限。
// 提示:Admin 应该包含所有权限
// 1. 定义 Permission 类型(枚举或 as const)
// 2. 实现 checkPermission(userPerms, requiredPerm)
// 你的代码:
参考答案
const Permission = {
Read: "READ",
Write: "WRITE",
Delete: "DELETE",
Admin: "ADMIN",
} as const;
type Permission = typeof Permission[keyof typeof Permission];
// "READ" | "WRITE" | "DELETE" | "ADMIN"
function checkPermission(
userPerms: Permission[],
required: Permission,
): boolean {
if (userPerms.includes(Permission.Admin)) return true;
return userPerms.includes(required);
}
// 测试
checkPermission(["READ", "WRITE"], "READ"); // true
checkPermission(["READ"], "DELETE"); // false
checkPermission(["ADMIN"], "DELETE"); // true
常见问题
问题一:数字枚举接受任意数字
enum Direction { Up, Down, Left, Right }
// 这个竟然不报错!
let d: Direction = 42; // 数字枚举的已知问题
解决:使用字符串枚举或 as const 对象替代。
问题二:const enum 跨模块报错
// 在 isolatedModules 模式下
const enum Foo { A = 1 }
// 跨文件导入 Foo.A 可能报错
解决:避免跨模块使用 const enum,或关闭 isolatedModules。
试一试
练习一
任务:定义一个 HTTP 方法枚举(GET, POST, PUT, DELETE),然后写一个接受该枚举参数的函数。
// 定义枚举 HttpMethod
// 写一个函数 sendRequest(url: string, method: HttpMethod)
// 你的代码:
参考答案
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
function sendRequest(url: string, method: HttpMethod): void {
console.log(`${method} ${url}`);
}
sendRequest("/api/users", HttpMethod.GET);
练习二
任务:用 as const 对象模拟一个用户角色枚举(Admin, Editor, Viewer),并提取出联合类型。
// 用 as const 定义角色常量对象
// 提取 Role 联合类型
// 你的代码:
参考答案
const Role = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER",
} as const;
type Role = typeof Role[keyof typeof Role];
// "ADMIN" | "EDITOR" | "VIEWER"
📝 要点总结
本章核心知识点
枚举类型
数字枚举(自增)、字符串枚举、异构枚举
编译产物
数字枚举生成双向映射对象,const 枚举内联消除
枚举 vs 联合
字面量联合更轻量,枚举适合有命名需求的场景
穷尽检查
枚举 + never 确保 switch 覆盖所有成员
最佳实践
推荐 const enum 或 as const + 联合替代普通枚举
下一章
枚举让你用命名常量替代魔法值。下一章我们学习泛型——让类型也能"传参数"。