Menu

TypeScript 枚举 enum:数字、字符串与 const 枚举

TypeScript 枚举(enum)是一组具名常量,例如 enum Direction { Up, Down }。学习数字枚举和字符串枚举、枚举编译后的 JavaScript、反向映射、遍历枚举、const 枚举,以及什么时候改用字符串字面量联合类型或 as const 对象更合适。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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),所以运行时没有任何开销,但无法遍历,而且逐个文件编译的工具会限制它的使用。

Coddy 编程语言插图

和 Coddy 一起学编程

开始