TypeScript 枚举与字面量类型 — 定海神针,一次讲透
程序里到处都是"固定的一组值":设备状态(运行/待机/故障)、告警级别(提示/警告/严重)、协议类型(MQTT/Modbus/OPC-UA)。用裸字符串管理它们,拼错一个字母就是运行时炸弹。枚举与字面量类型就是治理这种混乱的定海神针——今天我们把
enum的四种形态、字面量联合、as const三剑客彻底讲透,并给出工业项目的最终选型结论。
目录
一、为什么需要"固定的一组值"?
1.1 裸字符串的灾难现场
先看不用枚举的世界有多危险:
// 某人写的状态渲染
function renderStatus(status: string) {
if (status === "running") return "🟢 运行中";
if (status === "standby") return "🟡 待机";
if (status === "fault") return "🔴 故障";
return "❔ 未知";
}
// 三个月后,另一个人写的上报代码
function reportStatus(device) {
// ⚠ 拼错了!runing
sendToServer({ status: "runing" });
}
编译期零报错——因为 status: string 接受一切字符串。运行时 renderStatus("runing") 落到兜底分支,页面显示"❔ 未知",你可能三周后才发现产线数据一直在丢失。
1.2 魔法数字的灾难现场
// 这两个 2 和 3 是什么意思?
if (device.status === 2) {
// ...
}
setTimeout(refresh, 3 * 1000);
数字没有任何自解释性,靠口口相传记忆——第 2 个人接手项目时只能考古。
1.3 核心需求
我们需要一种手段满足三点:
限定取值范围:status 只能是三种之一,传别的编译期就报错
自解释:代码里写的是名字,不是魔法数字/拼错的字符串
双向可查:能从名字查值,也能从值反查名字(日志/调试用)
TypeScript 提供两大家族解决:enum(枚举) 和 字面量类型(literal types)。
二、字符串字面量联合:最轻量的方案
2.1 基本语法
第 1 天我们见过字面量类型的雏形。今天正式全面认识它:
// status 只能是这三个字符串之一
type DeviceStatus = "running" | "standby" | "fault";
let s: DeviceStatus;
s = "running"; // ✅
s = "standby"; // ✅
s = "runing"; // ❌ 编译错误:'"runing"' 不在 DeviceStatus 中
s = "RUNNING"; // ❌ 大小写敏感
拼错即刻爆红——1.1 的灾难在编译期就被拦截。
2.2 为什么叫"字面量类型"?
// 普通类型:描述"一类"值
let a: string; // 任意字符串
// 字面量类型:只描述"一个"值
let b: "running"; // 只能是字符串 "running"
// 字面量联合:若干个"精确值"的或
type DeviceStatus = "running" | "standby" | "fault";
"running" 本身就是一个类型,它的实例只有字符串 "running" 本身。这在 JS 层面不可想象,但 TS 的类型系统允许"值即类型"。
2.3 与 interface/type 组合
type DeviceStatus = "running" | "standby" | "fault";
type AlertLevel = "info" | "warn" | "error";
interface DeviceData {
id: string;
status: DeviceStatus; // 字面量联合做字段类型
alertLevel: AlertLevel;
}
const d: DeviceData = {
id: "CNC-001",
status: "running",
alertLevel: "warn"
};
// 自动补全体验:输入 d.status = " 时
// IDE 弹出 "running" | "standby" | "fault" 三个选项
2.4 字面量联合的短板
// 短板 1:值与类型耦合 —— 想拿到所有状态列表做下拉框?没法遍历类型
type DeviceStatus = "running" | "standby" | "fault";
// DeviceStatus 是编译期的东西,运行时不存在
// 想渲染 <select><option>running</option>... 必须手写一份运行时数组 → 两处维护
// 短板 2:从值反查"人类可读名"需要自己建映射
const STATUS_TEXT: Record<DeviceStatus, string> = {
running: "运行中",
standby: "待机",
fault: "故障"
};
字面量联合是纯类型层方案,运行时是隐形的——这既是优点(零产物、零开销)也是短板(运行时要用就得配套手写数据)。
2.5 练习
// 练习 1:定义 AlertLevel = "info" | "warn" | "error"
// 写 LEVEL_COLOR: Record<AlertLevel, string> 映射到颜色
// 提示/warn 黄/error 红
// 练习 2:定义 Protocol = "mqtt" | "modbus" | "opcua" | "ethernet"
// 写函数 getProtocolPort(p: Protocol): number(1883/502/4840/502…自查端口,写成常量映射)
三、数字枚举:传统 enum 的第一形态
3.1 基本语法与自动编号
// 数字枚举:不赋值则从 0 开始自动递增
enum DeviceStatus {
Running, // 0
Standby, // 1
Fault // 2
}
let s: DeviceStatus = DeviceStatus.Running; // 实际值 0
// 也可以自定义起始值
enum Level {
Info = 1, // 1
Warn, // 2
Error // 3
}
3.2 数字枚举的双向映射
enum 编译成真实的运行时对象,数字枚举还带反向查找:
enum DeviceStatus {
Running, // 0
Standby, // 1
Fault // 2
}
// 正向:名字 → 值
console.log(DeviceStatus.Running); // 0
// ⭐ 反向:值 → 名字(数字枚举特有)
console.log(DeviceStatus[0]); // "Running"
console.log(DeviceStatus[1]); // "Standby"
// 它的运行时真身(编译产物):
// var DeviceStatus;
// (function (DeviceStatus) {
// DeviceStatus[DeviceStatus["Running"] = 0] = "Running";
// DeviceStatus[DeviceStatus["Standby"] = 1] = "Standby";
// DeviceStatus[DeviceStatus["Fault"] = 2] = "Fault";
// })(DeviceStatus || (DeviceStatus = {}));
这就是 enum 与字面量联合的本质区别:enum 是类型 + 运行时对象的合体;字面量联合是纯类型,编译后消失。
3.3 数字枚举的坑
坑 1:枚举成员的类型就是数字
enum Level { Info = 1, Warn, Error }
let l: Level = Level.Warn;
// ⚠ 数字枚举成员可以直接赋 number!(类型系统开了后门)
let n: number = Level.Warn; // ✅ 合法:Level 可赋给 number
// 反过来不行
let l2: Level = 2; // ❌ 报错:number 不能赋给 Level
// ⚠ 但是!直接赋字面量数字却可以:
let l3: Level = 2; // ✅ 竟然合法!(2 是 Level.Warn 的字面量值)
数字枚举对数字字面量放行——let l3: Level = 2 编译通过。这让"限定取值"的承诺打了折扣。
坑 2:反向映射数组化的类型
// DeviceStatus[0] 的返回类型是 string,不是 DeviceStatus 的成员名联合
const name = DeviceStatus[0]; // string
// 且传越界数字不报错
console.log(DeviceStatus[99]); // undefined —— 静默失败
坑 3:跨文件/跨版本兼容
// 中间插入一个成员,所有后续数字变化!
enum DeviceStatus {
Running, // 0
Offline, // 1 ⚠ 新插入
Standby, // 2(原来是 1!)
Fault // 3(原来是 2!)
}
// 如果旧数据/持久化记录里存了 1(原 Standby),新代码读出来变成 Offline —— 数据错乱
这是数字枚举在工业系统里的高危坑:设备状态存了数据库,枚举中间插值后历史数据全部错位。规范:数字枚举必须显式赋值,永远不依赖自动递增。
3.4 练习
// 练习 1:定义显式赋值的 Protocol enum:Mqtt = 1883, Modbus = 502, OpcUa = 4840
// 写 getProtocolName(p: Protocol): string 用反向映射
// 练习 2:把 3.3 坑 3 的例子跑一遍,验证插入 Offline 后 Standby 的值变化
四、字符串枚举:可读的 enum 形态
4.1 基本语法
enum DeviceStatus {
Running = "RUNNING",
Standby = "STANDBY",
Fault = "FAULT"
}
let s = DeviceStatus.Running; // 值是 "RUNNING"(自解释!)
// ⚠ 字符串枚举没有反向映射
console.log(DeviceStatus["RUNNING"]); // undefined(数字枚举才有反查)
4.2 字符串枚举的编译产物
// 源码
enum DeviceStatus {
Running = "RUNNING",
Standby = "STANDBY"
}
// 编译产物:一个普通对象(比数字枚举干净,没有反向查表)
// var DeviceStatus;
// (function (DeviceStatus) {
// DeviceStatus["Running"] = "RUNNING";
// DeviceStatus["Standby"] = "STANDBY";
// })(DeviceStatus || (DeviceStatus = {}));
4.3 字符串枚举 vs 字面量联合
// 字符串枚举
enum StatusA {
Running = "RUNNING",
Standby = "STANDBY"
}
// 字面量联合
type StatusB = "RUNNING" | "STANDBY";
// ⚠ 关键区别:字符串枚举不兼容字面量!
let a: StatusA = StatusA.Running;
let a2: StatusA = "RUNNING"; // ❌ 报错!枚举要求用枚举成员访问
let b: StatusB = "RUNNING"; // ✅ 直接写字面量就行
// 这带来一个工程痛点:
function isFault(s: StatusA) { return s === StatusA.Fault; }
isFault("FAULT"); // ❌ 不能传裸字符串
isFault(StatusA.Fault); // ✅ 必须带命名空间
字符串枚举强制走 Enum.Member 命名空间——好处是来源明确、可读性强;代价是与 JSON/接口数据交互时必须转换(后端返回 "FAULT" 字符串,你不能直接当 StatusA 用)。
4.4 字符串枚举的真实价值场景
// 场景:状态值需要与后端/数据库协议严格一致,且要在运行时遍历
enum DeviceStatus {
Running = "RUNNING",
Standby = "STANDBY",
Fault = "FAULT"
}
// ✅ 运行时可遍历(下拉框、图例)
const statusOptions = Object.values(DeviceStatus);
// ["RUNNING", "STANDBY", "FAULT"]
// ✅ 值本身就是协议内容(日志可读)
console.log(`[监控] CNC-01 状态变更 → ${DeviceStatus.Fault}`);
// [监控] CNC-01 状态变更 → FAULT(而不是 2)
// ✅ 类型安全:写错成员名编译报错
const s = DeviceStatus.Falut; // ❌ 编译错误:Falut 不存在
4.5 练习
// 练习 1:定义字符串枚举 AlertLevel:Info/Warn/Error = "INFO"/"WARN"/"ERROR"
// 写 LEVEL_TEXT 映射中文,写 renderLevel(l: AlertLevel) 返回 "提示/警告/严重"
// 练习 2:验证字符串枚举不能反查:
// 枚举 AlertLevel,试 AlertLevel["INFO"],看结果
五、const 枚举与异构枚举:enum 的另类形态
5.1 const enum:编译期内联
const enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
function move(d: Direction) { /* ... */ }
move(Direction.Up);
编译产物(普通 enum vs const enum):
// 普通 enum:生成运行时对象
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
// ...
})(Direction || (Direction = {}));
move(Direction.Up);
// const enum:不生成对象,直接内联字面量
move("UP"); // ⭐ 值被直接替换进使用处
const enum 的特点:
⚠ 重要提醒:在 Vite/esbuild 等基于 isolatedModules 的现代构建工具中,
const enum历史上是不支持的(会直接报错或不内联)。TS 5.0 起 esbuild/流水线已能处理,但跨包使用 const enum 仍是雷区。初学阶段建议:知道它存在,项目里统一不用,需要零开销就用下一节的 as const 方案。
5.2 异构枚举(Heterogeneous Enum):不推荐
// 数字和字符串混着用 —— 官方文档明确不建议
enum Result {
Ok = 0,
Message = "OK"
}
异构枚举的成员类型不一致,双向映射混乱,纯属给自己挖坑。见到就重构。
5.3 enum 成员的冷知识:计算成员
// 枚举成员可以是计算值
enum Permission {
None = 0,
Read = 1 << 0, // 1(位运算)
Write = 1 << 1, // 2
Execute = 1 << 2, // 4
All = Read | Write | Execute // 7
}
// 位标志枚举:一个值同时表示多个权限(Linux 文件权限的经典思想)
const p = Permission.Read | Permission.Write; // 3 = 读+写
function canRead(perm: Permission): boolean {
return (perm & Permission.Read) !== 0;
}
位标志在工业协议(PLC 权限位、CAN 报文标志位)中真实存在,属于读得懂级别即可,日常业务优先级低。
5.4 练习
// 练习:用位标志 Permission 实现 canWrite(perm)、hasAll(perm)
// 验证 (Read | Write) & Write !== 0
六、as const + typeof:字面量的完全体
6.1 问题:字面量联合的两处维护
第二节留的尾巴——字面量联合是纯类型,运行时要配套手写数组:
type DeviceStatus = "running" | "standby" | "fault";
// 运行时需要列表(下拉框/图例),只能手写一份 ⚠
const STATUS_LIST = ["running", "standby", "fault"];
// ⚠ 新增状态时要改两处,漏一处就出 bug
6.2 as const:把数组"冻"成字面量元组
// 第一步:写运行时常量(唯一的真相源)
const STATUS_LIST = ["running", "standby", "fault"] as const;
// STATUS_LIST 的类型:readonly ["running", "standby", "fault"]
// 第二步:typeof + 索引访问,从常量推导类型(明天正式学,今天先看一眼)
type DeviceStatus = (typeof STATUS_LIST)[number];
// DeviceStatus = "running" | "standby" | "fault" ✅ 自动推导!
这 3 行代码就是现代 TS 的标准答案:
运行时:STATUS_LIST 是真实数组,能遍历能渲染下拉框
类型层:DeviceStatus 自动跟随 STATUS_LIST 推导
单一真相源:新增状态只改 STATUS_LIST 一处,类型自动更新
6.3 as const 的完整效果
const CONFIG = {
url: "ws://localhost:8080",
reconnect: true,
levels: ["info", "warn", "error"],
size: [1920, 1080]
} as const;
// CONFIG 的类型(每个属性都变成最深度的只读字面量):
// {
// readonly url: "ws://localhost:8080";
// readonly reconnect: true;
// readonly levels: readonly ["info", "warn", "error"];
// readonly size: readonly [1920, 1080];
// }
CONFIG.url = "x"; // ❌ 只读,不能改
CONFIG.levels.push("fatal"); // ❌ 只读数组没有 push
as const 做了三件事:
所有属性变
readonly所有值收窄为字面量类型(
true而不是boolean,"ws://..."而不是string)数组变
readonly元组(长度和每个位置的类型都锁定)
6.4 完整对比:同一需求的三种写法
// ===== 需求:告警级别,运行时要中文映射 + 列表,类型层要安全 =====
// ❌ 方案 A:纯字面量联合 + 手写数据(两处维护)
type Level1 = "info" | "warn" | "error";
const TEXT1 = { info: "提示", warn: "警告", error: "严重" } as const;
// ⚠ 方案 B:字符串枚举(运行时对象 + 需要映射表,与裸字符串不互通)
enum Level2 { Info = "info", Warn = "warn", Error = "error" }
// ✅ 方案 C:as const 对象(一个对象,类型和值全齐)
const LEVELS = {
info: { text: "提示", color: "#2196f3" },
warn: { text: "警告", color: "#ff9800" },
error: { text: "严重", color: "#f44336" }
} as const;
type Level = keyof typeof LEVELS; // "info" | "warn" | "error"(明天细讲 keyof)
type LevelInfo = (typeof LEVELS)[Level]; // { text: "提示"; color: "#2196f3" } | ...
// 使用:值与类型双全
function renderLevel(l: Level): string {
return LEVELS[l].text; // ✅ 映射和类型同源,永不脱节
}
renderLevel("warn"); // ✅
renderLevel("fatal"); // ❌ 编译错误
6.5 练习
// 练习 1:定义 PROTOCOLS = { mqtt: 1883, modbus: 502, opcua: 4840 } as const
// 推导 Protocol 类型;写 getPort(p: Protocol): number
// 练习 2:定义 STATUS = { running: {...}, standby: {...}, fault: {...} } as const
// 含 text/color/svg 三字段,写 renderStatusBadge(s: keyof typeof STATUS)
七、四大方案终极对比与选型
7.1 全维度对比表
7.2 选型决策树
需要"固定的一组值"?
│
├── 值是数字且有位运算需求(协议标志位/权限位)
│ └── ✅ 数字枚举(显式赋值!)或直接用常量位运算
│
├── 纯类型层使用(函数参数/返回值/字段标注),运行时不需要列表
│ └── ✅ 字面量联合(最轻,零开销)
│
├── 运行时需要列表/映射(下拉框、图例、颜色、文案),值要与后端协议一致
│ └── ✅ as const 对象 + keyof typeof(单一真相源,现代首选)
│
└── 团队规范已统一用 enum(存量代码一致性)
└── ✅ 字符串枚举(可读 + 可遍历),显式赋值
7.3 一句话结论
新项目默认 as const 对象(含映射信息时)或字面量联合(纯类型时);enum 只在需要遍历 + 团队规范统一时用字符串枚举、位运算场景用数字枚举。
这也是 Vue3/Pinia 等现代流行库源码的实际选择——它们的类型定义几乎全是字面量联合 + as const。
八、映射的钥匙:keyof 与枚举对象
8.1 Record + 联合 = 完整映射表
type DeviceStatus = "running" | "standby" | "fault";
// Record<K, V>:K 的每个成员都必须有对应的 V(明天细讲,今天记住形态)
const STATUS_CONFIG: Record<DeviceStatus, { text: string; color: string }> = {
running: { text: "运行中", color: "#00ff88" },
standby: { text: "待机", color: "#ffaa00" },
fault: { text: "故障", color: "#ff4444" }
};
// ✅ Record 的强制完整性:漏写一个成员编译报错
const BAD: Record<DeviceStatus, string> = {
running: "运行中",
standby: "待机"
// ❌ 缺 fault —— 编译错误:Property 'fault' is missing
}
8.2 enum 的 Record 映射
enum DeviceStatus {
Running = "RUNNING",
Standby = "STANDBY",
Fault = "FAULT"
}
const STATUS_CONFIG: Record<DeviceStatus, string> = {
[DeviceStatus.Running]: "运行中",
[DeviceStatus.Standby]: "待机",
[DeviceStatus.Fault]: "故障"
};
function renderStatus(s: DeviceStatus): string {
return STATUS_CONFIG[s]; // ✅ 类型保证映射完整
}
8.3 as const 对象的映射是"自带的"
// 第六节方案 C 的最大优势:映射即真相源,不存在"另建映射表"这步
const LEVELS = {
info: { text: "提示", color: "#2196f3" },
warn: { text: "警告", color: "#ff9800" },
error: { text: "严重", color: "#f44336" }
} as const;
// 遍历(下拉框):Object.entries 保留键值对
Object.entries(LEVELS).forEach(([key, cfg]) => {
console.log(key, cfg.text, cfg.color);
});
8.4 练习
// 练习:用 Record 给三种协议建 "默认端口 + 是否需要心跳" 的配置表
// mqtt: 1883/true, modbus: 502/false, opcua: 4840/true
// 故意漏写一个,看编译器报错信息
第九节占位说明
(本节并入第九节"穷尽检查"与第十节"实战" —— 保持目录连续编号。)
九、穷尽检查:字面量类型的守护神
9.1 复习 + 字面量版穷尽检查
第 4 天学过判别联合 + never 穷尽检查。字面量联合同样享受这一保护:
type DeviceStatus = "running" | "standby" | "fault";
function getIcon(status: DeviceStatus): string {
switch (status) {
case "running": return "🟢";
case "standby": return "🟡";
case "fault": return "🔴";
default: {
const _exhaustive: never = status; // ⭐ 守护
return _exhaustive;
}
}
}
9.2 新增状态的连锁保护
// 新增 "offline"(离线)状态
type DeviceStatus = "running" | "standby" | "fault" | "offline";
// getIcon 未更新 → default 里 status 是 "offline" ≠ never → 编译报错!
// 同样,STATUS_CONFIG: Record<DeviceStatus, ...> 也会立刻报缺成员
// ⭐ 这就是"单一真相源"的复利:类型一处改,所有遗漏处集体爆红
9.3 对象映射的穷尽(无 default 需求)
// Record 本身就是穷尽检查 —— 表里没写全就编译失败
// 比手写 switch 更防漏:switch 可能忘写 default + never,Record 忘写直接红
const STATUS_CONFIG: Record<DeviceStatus, string> = {
running: "运行中",
standby: "待机",
fault: "故障",
offline: "离线"
};
工程心法:能用 Record 映射就别写 switch——映射表是数据(可遍历、可扩展),switch 是逻辑(不可遍历、容易漏分支)。
十、实战场景全覆盖
10.1 场景一:设备状态体系(as const 完全体)
// ===== 状态体系:单一真相源 =====
export const DEVICE_STATUS = {
running: { text: "运行中", color: "#00ff88", icon: "🟢", level: 0 },
standby: { text: "待机", color: "#ffaa00", icon: "🟡", level: 1 },
fault: { text: "故障", color: "#ff4444", icon: "🔴", level: 2 },
offline: { text: "离线", color: "#7a8ba0", icon: "⚪", level: 3 }
} as const;
export type DeviceStatus = keyof typeof DEVICE_STATUS;
export type StatusConfig = (typeof DEVICE_STATUS)[DeviceStatus];
// ===== 使用 =====
function renderBadge(s: DeviceStatus): string {
const cfg = DEVICE_STATUS[s];
return `<span style="color:${cfg.color}">${cfg.icon} ${cfg.text}</span>`;
}
// 下拉框选项(运行时遍历)
const options = Object.entries(DEVICE_STATUS).map(([value, cfg]) => ({
value,
label: cfg.text
}));
// 判断逻辑(类型收窄自动跟随)
function isCritical(s: DeviceStatus): boolean {
return DEVICE_STATUS[s].level >= 2;
}
10.2 场景二:协议端口表(字面量联合 + Record)
type Protocol = "mqtt" | "modbus" | "opcua" | "ethernet";
interface ProtocolConfig {
port: number;
heartbeat: boolean;
desc: string;
}
export const PROTOCOLS: Record<Protocol, ProtocolConfig> = {
mqtt: { port: 1883, heartbeat: true, desc: "消息队列遥测传输" },
modbus: { port: 502, heartbeat: false, desc: "工控总线协议" },
opcua: { port: 4840, heartbeat: true, desc: "OPC 统一架构" },
ethernet: { port: 502, heartbeat: false, desc: "工业以太网" }
};
function connect(p: Protocol): string {
const cfg = PROTOCOLS[p];
return `连接 ${cfg.desc} @ ${cfg.port}${cfg.heartbeat ? "(心跳开启)" : ""}`;
}
10.3 场景三:WS 消息类型(字符串枚举版)
// 团队规范用 enum 的写法(对照学习)
export enum WsEvent {
Connect = "connect",
Data = "data",
Error = "error",
Close = "close"
}
// 消息类型定义(判别字段用枚举成员)
type WsMessage =
| { event: WsEvent.Connect; sessionId: string }
| { event: WsEvent.Data; payload: unknown }
| { event: WsEvent.Error; code: number; message: string }
| { event: WsEvent.Close; reason: string };
// 分发(第 4 天穷尽检查照样生效)
function handle(msg: WsMessage) {
switch (msg.event) {
case WsEvent.Connect: return `会话 ${msg.sessionId}`;
case WsEvent.Data: return `数据 ${msg.payload}`;
case WsEvent.Error: return `错误 ${msg.code}: ${msg.message}`;
case WsEvent.Close: return `关闭:${msg.reason}`;
default: {
const _exhaustive: never = msg;
return _exhaustive;
}
}
}
10.4 场景四:主题色板(as const 嵌套)
// 大屏主题:深色工业风色板(as const 嵌套对象)
export const THEME = {
dark: {
bg: "#0a1628",
panel: "#0f2035",
border: "#1a3a5c",
text: "#e0e8f0",
accent: "#00ff88",
series: ["#00ff88", "#ffaa00", "#3897f0", "#ff4444"]
},
light: {
bg: "#f5f7fa",
panel: "#ffffff",
border: "#dce4ec",
text: "#1a2a3a",
accent: "#00b368",
series: ["#00b368", "#e68a00", "#2f7fd6", "#d64545"]
}
} as const;
export type ThemeName = keyof typeof THEME; // "dark" | "light"
export type Theme = (typeof THEME)[ThemeName];
// 主题切换(类型跟随)
let current: ThemeName = "dark";
function getTheme(): Theme {
return THEME[current];
}
// 系列色循环取色
function pickColor(i: number): string {
const series = getTheme().series;
return series[i % series.length];
}
10.5 场景五:权限位标志(数字枚举的正当场景)
// 工业系统权限:读/写/配置/管理,用位表示可组合
export enum Permission {
None = 0,
Read = 1 << 0, // 0001
Write = 1 << 1, // 0010
Config = 1 << 2, // 0100
Admin = 1 << 3 // 1000
}
// 组合权限
const operator = Permission.Read | Permission.Write; // 0011
const engineer = Permission.Read | Permission.Write | Permission.Config;
// 检查权限
function has(perm: Permission, need: Permission): boolean {
return (perm & need) === need;
}
has(operator, Permission.Write); // true
has(operator, Permission.Config); // false
// 越权操作拦截
function tryConfig(perm: Permission): string {
if (!has(perm, Permission.Config)) {
return "❌ 权限不足:需要 Config 权限";
}
return "✅ 允许配置";
}
十一、类比记忆:路牌 vs 编号牌
把"固定的一组值"想成城市的管理方案:
十二、常见坑点与最佳实践
坑点 1:数字枚举自动递增 + 中途插值
// ❌ 依赖自动递增
enum Status { Running, Standby, Fault }
// 三个月后插入 Offline → 数据库历史数据错位
// ✅ 显式赋值
enum Status { Running = 0, Offline = 10, Standby = 20, Fault = 99 }
// 留出编号空间,插值不挪旧值
坑点 2:字符串枚举与裸字符串不互通
enum Level { Info = "INFO" }
// ❌ 后端返回的字符串不能直接用
const fromApi: string = "INFO";
const l: Level = fromApi; // 编译错误
// ✅ 需要守卫转换(又是第 4 天的知识!)
function isLevel(v: unknown): v is Level {
return Object.values(Level).includes(v as Level);
}
坑点 3:忘记 as const,联合退化
// ❌ 没有 as const:类型是 string[],推导不出字面量
const LEVELS = ["info", "warn", "error"];
type Level = (typeof LEVELS)[number]; // string(不是联合!)
// ✅ as const 后才是字面量元组
const LEVELS2 = ["info", "warn", "error"] as const;
type Level2 = (typeof LEVELS2)[number]; // "info" | "warn" | "error"
坑点 4:遍历数字枚举会把反向映射也遍历出来
enum Status { Running = 0, Standby = 1 }
Object.keys(Status); // ["0", "1", "Running", "Standby"] ⚠️
Object.values(Status); // ["Running", "Standby", 0, 1] ⚠️
// ✅ 数字枚举别直接遍历;要遍历用字符串枚举或 as const 对象
坑点 5:Record 映射与枚举键的写法
// ✅ 枚举做键用计算属性名
const M: Record<Status, string> = {
[Status.Running]: "运行中",
[Status.Standby]: "待机"
};
// ❌ 直接写字符串键(即使值相等)不匹配类型
const M2: Record<Status, string> = {
RUNNING: "运行中" // 编译错误(键类型不对)
};
坑点 6:const enum 在 Vite 中的兼容性
// ⚠ Vite(isolatedModules)环境下 const enum 可能报错或不内联
// 规则:同文件内用没问题;跨文件/跨包导出 const enum 是雷区
// 初学建议:不用 const enum,要零开销就 as const
坑点 7:字面量联合被宽类型吞噬
type Level = "info" | "warn";
// ❌ 接口字段写成 string,联合白定义
interface Bad { level: string }
// ✅ 字段类型精确到联合
interface Good { level: Level }
// ⚠ 注意数组推断
const arr = ["info", "warn"]; // string[]
const arr2 = ["info", "warn"] as const; // readonly ["info", "warn"]
最佳实践清单
新代码首选 as const 对象(带映射信息)或字面量联合(纯类型层)
enum 必须显式赋值(数字枚举防插值错位;字符串枚举保协议一致)
映射表用
Record<联合, 配置>,让编译器强制你写全下拉框/图例数据从 as const 对象遍历生成,单一真相源
switch 分发必带穷尽检查;但优先考虑 Record 映射替代 switch
不使用异构枚举;不跨包导出 const enum
外部字符串转枚举必须走守卫(第 4 天技能复用)
位标志场景(权限/协议位)用数字枚举 + 位运算
十三、自测挑战
Q1:type S = "a" | "b" 和 enum S { A = "a", B = "b" },运行时产物有什么区别?
Q2:数字枚举的反向映射是怎么实现的(编译产物角度)?
Q3:为什么数字枚举中间插入成员会导致历史数据错位?如何预防?
Q4:字符串枚举的值 "FAULT" 能直接赋给 Level 类型的变量吗?为什么?
Q5:const arr = ["a", "b"] 和 const arr = ["a", "b"] as const 的类型有什么区别?
Q6:写三行代码实现"从 ["info", "warn", "error"] 推导出联合类型"。
Q7:Record<DeviceStatus, string> 映射表漏写一个成员会发生什么?这为什么是好事?
Q8:遍历数字枚举的 Object.values() 会得到什么意外结果?
Q9:const enum 和普通 enum 的编译产物差异?为什么现代构建工具对它有兼容性顾虑?
Q10:位标志权限 Permission.Read | Permission.Write 的值是多少?has(perm, need) 怎么实现?
Q11:后端返回字符串 "running",如何安全转成 DeviceStatus 枚举/联合?(提示:守卫)
Q12:什么场景下你会放弃 as const 对象、选择数字枚举?
十四、总结与知识图谱
枚举与字面量类型(固定值的治理术)
│
├── 需求起源
│ ├── 裸字符串:拼错编译期无感知 → 运行时静默失败
│ └── 魔法数字:无自解释性 → 靠考古维护
│
├── 字面量联合(type A = "x" | "y")
│ ├── 值即类型,编译后消失(零开销)
│ ├── 拼错/大小写错误 → 编译报错
│ └── 短板:运行时不可遍历(需配套数据 → 两处维护)
│
├── enum 家族(类型 + 运行时对象合体)
│ ├── 数字枚举
│ │ ├── 自动递增(⚠ 插值错位,必须显式赋值)
│ │ ├── 反向映射(值 → 名,编译产物双写)
│ │ ├── ⚠ 数字字面量可直传(承诺打折)
│ │ └── 位标志场景(Permission:读/写/配置/管理)
│ ├── 字符串枚举
│ │ ├── 值自解释(日志友好)、可遍历
│ │ └── ⚠ 与裸字符串不互通(需守卫转换)
│ ├── const enum(编译期内联,跨包雷区,初学不用)
│ └── 异构枚举(❌ 见到就重构)
│
├── as const 三剑客(现代首选)
│ ├── as const:全只读 + 字面量化 + 元组化
│ ├── (typeof X)[number]:数组 → 联合
│ └── keyof typeof X:对象键 → 联合(明天正式学 keyof)
│ └── 价值:单一真相源(类型与数据永不脱节)
│
├── 配套设施
│ ├── Record<联合, 配置>:强制完整的映射表
│ └── 穷尽检查(never):新增成员 → 所有漏改处爆红
│
└── 选型决策树
├── 位运算/协议位 → 数字枚举(显式赋值)
├── 纯类型层 → 字面量联合
├── 需列表/映射/协议值 → as const 对象
└── 团队规范统一 → 字符串枚举
一句话总结:字面量联合是"零成本的类型路牌",enum 是"可遍历的运行时门牌",as const 对象是"类型与数据合一的市政花名册"——新项目优先花名册,纯类型用路牌,enum 留给位运算和团队规范。
延伸阅读
下一步
本文是 TypeScript 深入 系列的第 5 天。接下来的学习路线:
第 6 天:keyof / typeof / 索引访问 — 类型钥匙(typeof 已在今天预告,keyof 明天全面展开)
第 7 天:第一关 BOSS 战 — 综合实战(本周全部知识融成一个工业数据处理模块)
学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。
每天花 2 小时,28 天通关 TypeScript 深入。加油!