枚举

用一组命名常量,让代码自己说话

🎯 学习目标

学完本章,你将能够:

  • 能够定义数字枚举、字符串枚举和 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";
const enum 的坑:在跨模块使用时可能有问题(isolatedModules 模式下)。如果项目使用 Babel、esbuild 等非 TS 编译器,建议避免使用 const enum。

三种枚举对比

一张表看清差异

特性数字枚举字符串枚举const 枚举
自动赋值支持(从0递增)必须手动赋值支持
反向映射支持不支持不支持
编译输出生成 JS 对象生成 JS 对象内联字面量
运行时开销有有无
跨模块安全安全安全可能有坑
图:三种枚举的编译结果对比

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 友好
  • 不依赖编译器特殊处理
图:枚举方案选择决策树

推荐替代方案

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。

图:枚举替代方案对比

编译后的真相

数字枚举在 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 = {}));
图:数字枚举的双向映射结构

每一行做了两件事:把数字赋给名称(正向),再把名称赋给数字索引(反向)。所以 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
永远不要信任 API 返回的枚举值。后端可能新增了枚举成员、返回了意外值、甚至传了 null。一定要校验后再使用,否则运行时会收到意想不到的值。

何时用枚举

枚举 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"; // 报错!类型安全
迁移铁律:一次只改一个文件,每改一次跑一次测试。不要在整个项目里全局替换 enum —— 那样会导致编译错误雪崩,难以定位问题。优先迁移数字枚举(类型安全最弱),字符串枚举可以最后处理。

权限实战

练习

任务:定义一个权限系统的枚举或联合类型(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 + 联合替代普通枚举

下一章

枚举让你用命名常量替代魔法值。下一章我们学习泛型——让类型也能"传参数"。

1 / X