这节在地图里处理"状态常量"这件事。TS 给了两条路:enum 和字面量联合(含 const enum、字符串字面量)。它们编译产物差别很大,选错会拖慢启动或撑大包体。我们直接对比运行时行为。
enum 编译后是个真实对象,还带反向映射(值→名)。
enum Status { Pending, Paid, Shipped, } // 编译后约等于: // var Status = { 0:"Pending", 1:"Paid", 2:"Shipped", Pending:0, Paid:1, Shipped:2 } Status.Paid; // 1 Status[1]; // "Paid" 反向映射
输入输出:Status.Paid 得 1,Status[1] 得 "Paid"。反向映射方便调试打印,但代价是运行时多了一个对象、更多 JS 代码。在强调包体的前端场景,这是净负担。
enum Role { Admin = "admin", Guest = "guest", } Role.Admin; // "admin" // Role["admin"] 不存在,字符串枚举没有反向映射
字符串枚举编译后仍是对象,但更安全:序列化到后端、写日志都直接用 "admin",不暴露数字序号。
const enum 在编译时被内联成字面量,不生成对象。
const enum Color { Red = "red", Green = "green", } let c = Color.Red; // 编译后直接是 let c = "red"
输入输出:产物里 Color 根本不存在,c 直接赋 "red"。零运行时成本。代价:不能动态 Color[key] 取反,且跨文件需 preserveConstEnums 或同一编译单元。这在库代码里要谨慎——分离编译时 const enum 会出问题。
不想要任何运行时对象,就用字面量联合(2.3 讲过基础)。
type StatusLit = "pending" | "paid" | "shipped"; function next(s: StatusLit) { /* ... */ } next("paid"); // OK next("x"); // 报错
它编译后完全消失(和 const enum 一样内联),且拼错即报错。对比 enum,它不能"反向拿到名字列表来渲染下拉框",但多数业务不需要。
这张图把四种写法的"运行时残留"排个序:

// 场景:订单状态要在前端下拉框枚举展示 // 方案A:数字 enum,能 Status[1] 直接拿中文名 enum OrderStatus { Pending=0, Paid=1, Done=2 } // 方案B:字面量联合,下拉框数据得另外维护一份映射 type OrderStatusLit = "pending" | "paid" | "done"; const LABELS: Record<OrderStatusLit, string> = { pending: "待支付", paid: "已支付", done: "完成", };
背景:既要类型安全又要 UI 展示。操作:enum 自带反向名,但多对象;字面量联合要手写 LABELS 映射。结果:两者都能用,成本不同。解读:枚举的"反向映射"在需要列名字时很值,否则是浪费。
enum 允许数字和字符串混在一个枚举里,但极度损伤可读性,几乎没有合理场景,写出来别人看不懂,别用。
enum 成员可在 switch 里做可辨识联合式的收窄,配合穷尽检查能在加新状态时逼你处理所有分支。
enum Mode { Dev, Prod, Test } function start(m: Mode) { switch (m) { case Mode.Dev: console.log("开发"); break; case Mode.Prod: console.log("生产"); break; // 若以后新增 Mode.Test 却忘加 case,这里的 exhaustiveness 会报错 default: { const _never: never = m; return _never; } } }
背景:状态机、环境切换要穷尽所有分支。操作:末尾 default 把 m 赋给 never。结果:一旦漏处理新成员,赋值 never 直接编译失败。解读:这是用类型系统兜底"忘了加 case"的经典手法,字面量联合同样适用。
as const 锁定普通字面量声明会"拓宽"成基类型,用 as const 能把它钉成精确字面量,常用于配置与常量表。
const env = "prod"; // 类型 string,可改 const env2 = "prod" as const; // 类型 "prod",不可改 // 数组用 as const 锁成只读元组 const levels = ["low", "mid", "high"] as const; type Level = typeof levels[number]; // "low" | "mid" | "high" function setLevel(l: Level) { /* ... */ } setLevel("mid"); // OK // setLevel("top"); // 报错:不在字面量集合
背景:配置项、权限等级想"既当值用又当类型用"。操作:用 as const 同时获得精确字面量类型与只读保护。结果:取值与类型同源,不会出现"值字符串"和"类型字面量"两份不一致定义。解读:as const 是字面量联合的"值侧"生成器,比手敲 type X = "a"|"b" 更不易漂移。
当需要从一份数据同时得到"运行时值集合"和"编译期联合类型",as const 往往比 enum 更省心:
const PERMS = ["read", "write", "admin"] as const; type Perm = typeof PERMS[number]; // "read" | "write" | "admin" // 运行时遍历 PERMS.forEach((p) => console.log(p)); // 等价 enum 写法需单独处理迭代与类型两份: // enum PermE { Read="read", Write="write", Admin="admin" } // 反向遍历需 Reflect 或手写数组,样板更多
背景:权限、标签这类既要遍历又要有类型的地方很多。操作:优先 as const + typeof。结果:一份定义同时服务运行时与编译期。解读:这进一步说明"新项目默认字面量联合"的工程取向——除非真要反向映射。
我们现在的默认是字面量联合,除非明确需要反向映射或要兼容旧 JS 枚举代码。const enum 因跨文件编译问题在库里慎碰,应用项目(单编译单元)可以放心用。
字面量联合没有反向名字列表,但配合类型守卫能写出安全的分支处理,比 enum 更轻且零产物。
type Direction = "north" | "south" | "east" | "west"; const LABELS: Record<Direction, string> = { north: "北", south: "南", east: "东", west: "西", }; function describe(d: Direction): string { return `朝向${LABELS[d]}`; // d 被窄为具体字面量,LABELS[d] 精确 } describe("north"); // "朝向北" // describe("up"); // 报错:不在联合内
输入输出:describe("north") 返回 "朝向北",拼错方向直接编译失败。这里 LABELS 是手写的一份映射,但因为 Direction 是字面量联合,漏写某个键编译器会立刻报——比 enum 反向映射更安全(enum 漏写不会报错)。
老代码用数字 enum,想逐步切到字面量联合而不一次性重写,可以双轨并存、逐个替换调用点。
// 旧:数字 enum enum Legacy { A = 0, B = 1 } // 新:字面量联合,逐步替换 type New = "a" | "b"; // 兼容桥:旧值转新值,错误旧值落到 never 由编译器提示 function toNew(v: Legacy): New { if (v === Legacy.A) return "a"; if (v === Legacy.B) return "b"; // 若以后新增 Legacy.C 却忘在此处理,会编译报错,逼你补全 throw new Error("unreachable"); }
背景:大项目不能一口气删 enum。操作:保留旧 enum,新代码用联合,用桥函数收敛。结果:旧调用点不动,新代码享受联合的轻量。解读:这种"绞杀者"式迁移让契约升级零停机,也借编译器防漏。
⚠️ 别在会被单独编译发布的库里用 const enum。分离编译时,消费方拿不到枚举定义会直接编译失败;应用内单编译单元则可放心。
💡 只想要"有限候选 + 拼错报错",用字面量联合;要"反向拿到名字渲染 UI",再考虑数字 enum,别为小便利背上运行时对象。