TypeScript 枚举(enum)是一组具名常量。enum Direction { Up, Down, Left, Right } 同时创建了一个类型 Direction 和一个运行时对象,你可以用 Direction.Up 访问它的成员。如果不指定值,成员从 0 开始编号;字符串枚举则为每个成员提供一个可读的字符串。
枚举是少数不只是类型的 TypeScript 特性之一:代码编译后,枚举会变成一个真正的 JavaScript 对象。
数字枚举
不写初始值时,成员依次得到 0、1、2 等值。给第一个成员指定一个数字,后面的成员会从它开始继续递增。你也可以显式地为每个成员赋值,当这些数字要存进数据库或通过网络发送时,这是更稳妥的做法。
对于从不离开程序的值,依赖自动编号没有问题。但如果成员的顺序可能改变,而这些数字又被持久化保存在某处,那么在中间插入一个成员,就会悄悄地给后面所有成员重新编号。
枚举编译成了什么
类型会被擦除,但枚举不会。下面是 TypeScript 为一个数字枚举和一个字符串枚举生成的 JavaScript:
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
Direction["Up"] = 0 返回 0,所以同一条语句里也设置了 Direction[0] = "Up"。因此数字枚举是双向映射的:从名称到数字,也能从数字回到名称。这就是反向映射。字符串枚举只有从名称到值的映射。
打印出来的 Direction 对象有八个键:四个名称和四个数字。一旦你要遍历它,这一点就很重要。
字符串枚举
字符串枚举的每个成员都需要一个显式的字符串值。这些值会原样出现在日志、JSON 和数据库中,所以字符串枚举比数字更容易调试。
字符串枚举在一个方面是名义类型的,这常常让人意外:即使文本和某个成员的值完全一样,普通字符串也不能赋值给它。
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(报错信息里的建议是编译器猜的,在这里并不对;正确的写法是 Status.Active。)反过来,Status 值可以用在任何需要 string 的地方。当值以字符串形式从 JSON 或表单传进来时,用下文“检查值是否属于枚举”一节中的方法先检查再转换。
把枚举当作类型
枚举名就是一个类型,它的取值就是它的成员。结合 switch 使用时,如果函数必须返回一个值,TypeScript 会检查每个成员是否都被处理了:
如果给 Shape 新增了成员却没有加上对应的 case,sides 就会因为 TS2366 无法编译:Function lacks ending return statement and return type does not include 'undefined'.。switch 页面介绍了更严格的、基于 never 的穷尽检查。
最后几行暴露了数字枚举的一个真正弱点。不匹配任何成员的数字字面量,比如 const level: Level = 99,会产生编译错误(TS2322),但任何类型为 number 的值都会被接受,所以 57 能蒙混过关。字符串枚举没有这个漏洞。
遍历枚举
枚举在运行时是一个对象,所以 Object.keys、Object.values 和 Object.entries 都能用。对于字符串枚举,它们返回的正好是所有成员。对于数字枚举,它们还会返回反向映射的条目,需要过滤掉:
如果想把变量的类型声明为“枚举的某个成员名”,用 keyof typeof Direction,它就是联合类型 "Up" | "Down" | "Left" | "Right"。之后用 Direction[name] 查找值,能得到完整的类型安全。
字符串枚举没有反向映射,所以要根据值得到成员名,就得在条目里查找:Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] 的结果是 "Active",如果没有成员拥有这个值,则是 undefined。
检查值是否属于枚举
来自程序外部的数据只是普通的 string 或 number。类型守卫会把它和枚举的值做比对,并把它收窄为枚举类型:
不要对不可信的输入使用 raw as Status:这个断言能通过编译,但运行时什么都没检查,于是 "DELETED" 会被当作合法的 Status 在程序里到处传递。
const 枚举
const enum 要求编译器删除这个枚举,并在每个使用的地方直接写入成员的值。运行时不存在任何对象,所以既不能遍历,也没有反向映射。
const enum 能省下几个字节和一次属性查找,但前提是编译器在编译每个使用它的文件时,都能看到枚举的声明。逐个文件转译的工具(例如 Babel 和 swc)看不到在其他文件中声明的 const 枚举;Node 的类型剥离和对待其他枚举一样拒绝 const 枚举;而在开启 isolatedModules 或 verbatimModuleSyntax 时,使用声明文件中的 const 枚举,TypeScript 会报 TS2748 错误。大多数应用代码都不需要 const 枚举。
枚举、联合类型与 as const 对象的对比
定义一组固定值有三种常见方式:
enum | 字面量联合类型 | as const 对象 | |
|---|---|---|---|
| 运行时存在 | 是,一个对象 | 否 | 是,一个普通对象 |
| 遍历所有值 | Object.values(数字枚举需过滤) | 否,没有可遍历的东西 | Object.values |
接受普通的 "red" | 否(字符串枚举) | 是 | 是 |
具名访问 X.Red | 是 | 否 | 是 |
| 反向映射 | 仅数字枚举 | 否 | 否 |
| 能在 Node 类型剥离下运行 | 否 | 是 | 是 |
erasableSyntaxOnly 允许 | 否 | 是 | 是 |
| 需要额外学习的语法 | 枚举规则、const 枚举 | 无 | typeof 写法 |
如今很多团队默认使用字符串字面量联合类型,在运行时需要这些值(用来遍历或生成下拉菜单)时,再改用 as const 对象。原因是:联合类型是纯粹的类型,会从输出中消失;它们能接受 JSON 和 API 传来的普通字符串;而枚举是日常 TypeScript 中唯一一个不属于“JavaScript 加上可擦除类型”的部分。
最后这一点已经有了实际影响。Node 可以通过剥离类型直接运行 .ts 文件,而枚举是它无法剥离的:
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Node 的 --experimental-transform-types 标志可以让枚举运行起来,而编译器选项 erasableSyntaxOnly 会把每个枚举都报告为 TS1294 错误:This syntax is not allowed when 'erasableSyntaxOnly' is enabled.,所以项目可以一开始就禁止使用枚举。类型剥离的原理见运行 TypeScript。这些都不代表枚举是错的:用 tsc 或打包工具编译的代码可以正常运行枚举,已经在使用枚举的代码库改写后收益也不大。
常见问题
TypeScript 中的枚举是什么?
一组具名常量,它既是一个类型,又是一个运行时对象:enum Direction { Up, Down } 让你可以写 Direction.Up,也可以把 Direction 当作参数类型。和大多数 TypeScript 特性不同,枚举不会被擦除:它会编译成一个在运行时真实存在的 JavaScript 对象。
TypeScript 中如何遍历枚举?
对于字符串枚举,Object.values(MyEnum) 得到所有值,Object.keys(MyEnum) 得到所有名称。数字枚举还包含反向映射的条目("0": "Up"),所以要把它们过滤掉:Object.keys(Direction).filter((k) => isNaN(Number(k))) 只返回名称。const enum 无法遍历,因为它在运行时并不存在。
TypeScript 中如何把字符串转换成枚举值?
在类型守卫里把字符串和枚举的值做比对:function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }。检查通过后,s 的类型就是 Status。直接写 s as Status 虽然能编译,但运行时什么也不检查。
TypeScript 中应该用枚举还是联合类型?
很多团队更喜欢字符串字面量联合类型(type Status = "active" | "inactive"),如果运行时也需要这些值,就用 as const 对象。联合类型会被完全擦除,兼容 Node 内置的类型剥离和 erasableSyntaxOnly 选项,还能直接接受 "active" 这样的普通字符串。枚举也没有问题,尤其是在已经使用枚举的代码库里。
enum 和 const enum 有什么区别?
普通枚举会编译成一个对象,运行时可以遍历和查找。const enum 在编译时被删除,每一处使用都替换成对应的值(Size.Large 变成 2),所以运行时没有任何开销,但无法遍历,而且逐个文件编译的工具会限制它的使用。