前置知识: JavaScript

枚举进阶

22 min中级

枚举高级用法与替代方案

阅读提示:正文以代码和白话为主,不出现类型论公式。进阶文档中若出现 Γ ⊢ e : τ 这类记号,第一遍可完全跳过(完整规则见 001-HowToReadThisCourse)。

枚举进阶

前置知识

学习目标

  • 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
  • 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱

本文档对标 MIT 6.S192、Stanford CS110、CMU 15-214 等课程教学水准,系统讲解 TypeScript 枚举(enum)类型的设计动机、形式化定义、运行时行为、与企业级替代方案。所有代码示例均可在 TS 5.4 + strict: true 下编译通过。


1. 历史动机与发展脉络

1.1 枚举的起源与设计动机

枚举(enumeration)作为一种类型构造子,最早可追溯到 1975 年 Pascal 语言规范的”标量类型”。C 语言在 1978 年 K&R 第二版中正式引入 enum 关键字,将其定义为”映射到整型的命名常量集合”。这一设计哲学深刻影响了后续 C++、Java、C# 等静态类型语言。

TypeScript 的设计者 Anders Hejlsberg(同时是 Turbo Pascal、Delphi、C# 的首席架构师)在 2012 年发布 TypeScript 0.8 时,自然沿用了 C# 的枚举语义:

  • C# 1.0(2002):enum Color { Red, Green, Blue } 默认映射到 int,可显式指定底层类型
  • TypeScript 0.8(2012):完全照搬 C# 语义,引入 enum 关键字
  • TypeScript 0.9(2013):新增 const enum,用于编译期内联优化
  • TypeScript 1.5(2015):枚举成员类型(literal enum members)支持,使 ShapeKind.Circle 成为可区分联合的判别字段
  • TypeScript 2.0(2016):--strictNullChecks 引入后,字符串枚举成为更安全的默认选择
  • TypeScript 2.4(2017):正式支持字符串枚举(之前仅支持数字枚举),并引入异构枚举
  • TypeScript 3.4(2019):as const 断言发布,提供枚举的现代替代方案
  • TypeScript 4.5(2021):const 断言在 const enum 上的语义对齐
  • TypeScript 5.0(2023):装饰器 Stage 3 落地,配合 as const 可实现声明式枚举校验
  • TypeScript 5.4(2024):NoInfer<T> 工具类型,使基于 as const 的枚举推断更精确

1.2 设计动机分析

Anders Hejlsberg 在 2017 年 GopherCon 的访谈中阐述 TypeScript 枚举的两个核心动机:

动机一:与 JavaScript 互操作的零成本抽象。 TypeScript 必须能编译为可读、可调试的 JavaScript。enum 编译后产生一个普通对象,开发者可在 DevTools 中直接观察其反向映射,无需 sourcemap。

动机二:捕获 C#/Java 移民工程师的心智模型。 TypeScript 需要降低企业级后端团队迁移成本,保留 enum 关键字使代码读起来”像 C#”。

这两个动机导致 TypeScript enum 在 2012-2019 年间保留了三个”非 JavaScript 原生”特性:

  1. 运行时存在:enum 不是纯类型,编译产物中确实存在一个对象
  2. 反向映射:数字枚举成员 Direction.Up = 0 会同时产生 Direction[0] = 'Up'
  3. 结构化不兼容:枚举类型与字面量联合类型在结构上不兼容,无法互换赋值

1.3 TypeScript 版本时间线

2012-10  TS 0.8     enum 关键字首次出现
2013-06  TS 0.9     const enum 引入
2015-07  TS 1.5     枚举成员类型(用于可区分联合)
2017-06  TS 2.4     字符串枚举、异构枚举
2019-03  TS 3.4     as const 断言(替代方案的起点)
2021-11  TS 4.5     const enum 在 isolatedModules 下的行为修正
2023-03  TS 5.0     装饰器 Stage 3,可声明式校验枚举值
2024-03  TS 5.4     NoInfer<T>,配合 as const 提升推断精度

1.4 当前社区共识(2024-2025)

TypeScript 核心团队在 TypeScript Roadmap 2024 中明确表态:

  • 不再为 enum 添加新特性:枚举语法已稳定,未来工作集中在 as const 与 satisfies 路线
  • const enum 不推荐用于库代码:在 isolatedModules: true 下行为不一致,Babel、esbuild、swc 等工具不支持
  • as const 对象 + 联合类型已成为社区事实标准,被 Airbnb、Google、Vercel 等公司风格指南推荐

2. 形式化定义

2.1 类型论视角

在 TypeScript 的类型系统中,枚举可形式化定义为一个 命名常量域(named constant domain)与一个 底层值域(underlying value domain)之间的双射:

EnumT≜{(ki,vi)∣ki∈Identifiers,vi∈T}\text{Enum}_T \triangleq \{ (k_i, v_i) \mid k_i \in \text{Identifiers}, v_i \in T \}

其中 T∈{number,string}T \in \{\text{number}, \text{string}\} 为底层类型。每个枚举成员 (ki,vi)(k_i, v_i) 同时定义:

  • 一个值(value):可在运行时使用,类型为 TT
  • 一个类型(type):可在类型位置使用,表示单例字面量类型

这一双值-类型语义与 Haskell 的 data 类型、Rust 的 enum 类型有本质差异——后两者是和类型(sum type),而 TypeScript enum 仅为命名常量集合。

2.2 编译期与运行时语义

TypeScript enum 在类型系统中的形式化规约(参考 [TypeScript Specification 4.6, §9.2]):

Γ⊢e:enum{k1:v1,…,kn:vn}Γ⊢e.ki:Tvi(value position)\frac{\Gamma \vdash e : \text{enum}\{k_1: v_1, \ldots, k_n: v_n\}}{\Gamma \vdash e.k_i : T_{v_i} \quad \text{(value position)}} Γ⊢e:enum{k1:v1,…,kn:vn}Γ⊢e.ki:literal(vi)(type position)\frac{\Gamma \vdash e : \text{enum}\{k_1: v_1, \ldots, k_n: v_n\}}{\Gamma \vdash e.k_i : \text{literal}(v_i) \quad \text{(type position)}}

关键点:枚举成员在值位置与类型位置具有不同的类型含义。

  • 值位置:Direction.Up 的类型为 Direction(整个枚举)
  • 类型位置:Direction.Up 作为类型表示单例字面量 0

2.3 编译产物的形式化

数字枚举 enum Direction { Up, Down, Left, Right } 编译后的 JavaScript 产物等价于:

"use strict";
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 = {}));

可形式化为一个双向映射对象 MM:

M={ki↦vi}∪{vi↦ki∣vi∈N}M = \{ k_i \mapsto v_i \} \cup \{ v_i \mapsto k_i \mid v_i \in \mathbb{N} \}

字符串枚举仅保留单向映射 {ki↦vi}\{ k_i \mapsto v_i \},因为字符串到名称的反向映射会破坏字符串作为业务标识符的语义。

2.4 as const 对象的形式化

as const 对象 + 联合类型的现代替代方案:

const Direction = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;

type Direction = (typeof Direction)[keyof typeof Direction];
// 等价于 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'

其形式化定义为:

ConstObjT≜{(ki,vi)∣ki∈Identifiers,vi∈LiteralT}\text{ConstObj}_T \triangleq \{ (k_i, v_i) \mid k_i \in \text{Identifiers}, v_i \in \text{Literal}_T \} UnionT≜⨆i=1nliteral(vi)\text{Union}_T \triangleq \bigsqcup_{i=1}^{n} \text{literal}(v_i)

与 enum 相比,as const 对象在运行时是一个冻结的普通对象,在类型层是一个字面量联合类型。两者在结构化类型系统中是同构的,但在结构兼容性上是不同的:enum 是 nominal-like(名义子类型倾向),as const 联合类型是 structural(结构子类型)。


3. 理论推导与原理解析

3.1 反向映射的数学结构

对于数字枚举,TypeScript 编译产物构造的反向映射可视为一个自反函数 f:Names↔Numbersf: \text{Names} \leftrightarrow \text{Numbers}:

f(ki)=i,f−1(i)=kif(k_i) = i, \quad f^{-1}(i) = k_i

当枚举值显式指定时(如 enum E { A = 10, B = 20 }),ff 不再是单调的,但仍保持单射性(同一值不可重复)。若发生值重复,TypeScript 不会报错,但反向映射会丢失信息:

enum E {
  A = 1,
  B = 1, // 重复值,E[1] === 'B'
}
console.log(E[1]); // 'B'(覆盖了 'A')

形式化分析:

∀ki,kj∈Names:vi=vj  ⟹  f−1(vi)=kj(后定义者胜出)\forall k_i, k_j \in \text{Names}: v_i = v_j \implies f^{-1}(v_i) = k_j \quad (\text{后定义者胜出})

3.2 字符串枚举为何不提供反向映射

字符串枚举 enum Status { Active = 'ACTIVE' } 编译后为:

var Status;
(function (Status) {
  Status["Active"] = "ACTIVE";
})(Status || (Status = {}));

不构造反向映射 Status["ACTIVE"] = "Active" 的原因有二:

  1. 业务标识符语义:字符串值 'ACTIVE' 是业务概念,反向映射会污染对象的属性命名空间
  2. 类型安全考量:若提供反向映射,则 Status['ACTIVE'] 在类型层应为何?类型系统无法表达”反向查询返回成员名”这一复杂语义

形式化解释:字符串域 Σ∗\Sigma^* 与标识符域 Identifiers⊂Σ∗\text{Identifiers} \subset \Sigma^* 在数据上同构,但在类型系统中不是同构的——字符串字面量 'ACTIVE' 既是枚举值又是潜在的反向键名,会引起歧义。

3.3 const enum 的内联优化

const enum 在编译期被完全内联,不产生运行时对象。形式化地说:

const enum E 在编译产物中等价于 {e.ki↦vi}\text{const enum } E \text{ 在编译产物中等价于 } \{ e.k_i \mapsto v_i \}

源代码:

const enum Color { Red = '#FF0000', Green = '#00FF00' }
const c = Color.Red;

编译产物:

"use strict";
const c = "#FF0000" /* Color.Red */;

数学上,const enum 是一个纯类型层构造(type-level construct),运行时仅保留值替换。这要求所有引用点都能在编译期解析到具体值,因此:

  • const enum 不能被 Object.keys() 遍历
  • const enum 不能被字符串名动态访问(如 Color['Red'] 在 --isolatedModules 下报错)
  • const enum 在跨文件使用时要求编译器能”看到”定义(Babel、esbuild 不支持)

3.4 as const 与不可变性

as const 断言的形式化语义为:将推断出的 widened 类型替换为字面量类型,并标注所有属性为 readonly。

Γ⊢e:{ki:Ti}Γ⊢e as const:{readonly ki:literal(Ti)}\frac{\Gamma \vdash e : \{ k_i: T_i \}}{\Gamma \vdash e \text{ as const} : \{ \text{readonly } k_i: \text{literal}(T_i) \}}

其中 literal(Ti)\text{literal}(T_i) 将 widened 类型 TT 收窄到具体的字面量类型。例如:

  • string → 'UP'
  • number → 42
  • boolean → true
  • 数组 → readonly tuple

这一推导是 TypeScript 3.4 引入的核心机制,是现代枚举替代方案的基石。

3.5 可区分联合与枚举成员类型

枚举成员作为类型时,可作为可区分联合(discriminated union)的判别字段:

enum ShapeKind { Circle, Square }

interface Circle {
  kind: ShapeKind.Circle;  // 单例字面量类型 0
  radius: number;
}

interface Square {
  kind: ShapeKind.Square;  // 单例字面量类型 1
  size: number;
}

type Shape = Circle | Square;

function area(s: Shape): number {
  switch (s.kind) {
    case ShapeKind.Circle:
      return Math.PI * s.radius ** 2;  // s 收窄为 Circle
    case ShapeKind.Square:
      return s.size ** 2;              // s 收窄为 Square
  }
}

形式化推导(基于控制流分析):

Γ⊢s:Circle⊔Squares.kind=ShapeKind.CircleΓ⊢s:Circle\frac{\Gamma \vdash s : \text{Circle} \sqcup \text{Square} \quad s.\text{kind} = \text{ShapeKind.Circle}}{\Gamma \vdash s : \text{Circle}}

这一推导依赖 TypeScript 2.0 引入的控制流类型收窄(control flow type narrowing)机制,可视为基于判别字段(discriminant)的子类型消解。

3.6 编译产物的体积模型

设枚举 EE 有 nn 个成员,则:

  • 数字枚举产物大小:O(n)O(n) 个赋值语句 + 反向映射
  • 字符串枚举产物大小:O(n)O(n) 个赋值语句,无反向映射
  • const enum 产物大小:O(0)O(0)(无对象产生,引用点替换为字面量)
  • as const 对象产物大小:O(n)O(n) 个属性(普通对象字面量)

对于大型项目,使用 const enum 或 as const 可显著减小 bundle 体积,且对 tree-shaking 友好。


4. 代码示例

4.1 数字枚举基础

// TS 5.4, tsconfig.json: { "strict": true }

/**
 * 方向枚举 - 数字枚举示例
 * 用于表达 4 个基本方向,自动从 0 开始递增
 */
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right,   // 3
}

/**
 * 显式起始值的数字枚举
 * 起始为 1,常用于避免与 falsy 值 0 冲突
 */
enum HttpStatus {
  Ok = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500,
}

// 数字枚举的反向映射特性
console.log(Direction.Up);       // 0
console.log(Direction[0]);       // 'Up'(反向映射)
console.log(HttpStatus[200]);    // 'Ok'

// 数字枚举与 number 的结构兼容性
const code: number = HttpStatus.Ok;  // 允许:enum 是 number 的子类型

4.2 字符串枚举

/**
 * 用户状态 - 字符串枚举
 * 字符串值便于序列化、日志输出、跨服务传输
 */
enum UserStatus {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
  Pending = 'PENDING',
  Suspended = 'SUSPENDED',
}

/**
 * API 响应中的状态字段
 * 字符串枚举确保序列化后类型信息可保留
 */
interface User {
  id: string;
  name: string;
  status: UserStatus;
}

// 序列化与反序列化
const user: User = {
  id: 'u-001',
  name: 'Alice',
  status: UserStatus.Active,
};

const json = JSON.stringify(user);
// {"id":"u-001","name":"Alice","status":"ACTIVE"}

const parsed = JSON.parse(json) as User;
console.log(parsed.status === UserStatus.Active);  // true

4.3 异构枚举(不推荐)

/**
 * 异构枚举 - 数字与字符串混合
 * 警告:TypeScript 官方明确不推荐,仅为兼容性保留
 */
enum BooleanLikeHeterogeneous {
  No = 0,
  Yes = 'YES',
}

// 实际场景中应拆分为两个独立枚举
enum BooleanNumeric {
  No = 0,
  Yes = 1,
}

enum BooleanString {
  No = 'NO',
  Yes = 'YES',
}

4.4 const enum 编译期内联

// 注意:const enum 在 isolatedModules: true 下不推荐使用
// 适用于应用内部代码,不适用于发布为 npm 包的库

const enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3,
}

const currentLevel = LogLevel.Info;  // 编译为:const currentLevel = 1;

if (currentLevel >= LogLevel.Warn) {
  // 编译为:if (currentLevel >= 2)
  console.warn('Warning level reached');
}

4.5 枚举成员类型与可区分联合

// TS 5.4 - 可区分联合使用枚举成员作为判别字段

enum RequestKind {
  Get,
  Post,
  Put,
  Delete,
}

interface GetRequest {
  kind: RequestKind.Get;
  url: string;
  params?: Record<string, string>;
}

interface PostRequest {
  kind: RequestKind.Post;
  url: string;
  body: unknown;
}

interface DeleteRequest {
  kind: RequestKind.Delete;
  url: string;
}

type HttpRequest = GetRequest | PostRequest | DeleteRequest;

function send(req: HttpRequest): Promise<Response> {
  switch (req.kind) {
    case RequestKind.Get: {
      const search = new URLSearchParams(req.params);
      return fetch(`${req.url}?${search}`);
    }
    case RequestKind.Post:
      return fetch(req.url, {
        method: 'POST',
        body: JSON.stringify(req.body),
      });
    case RequestKind.Delete:
      return fetch(req.url, { method: 'DELETE' });
  }
}

4.6 as const 现代替代方案(推荐)

// TS 5.4, tsconfig.json: { "strict": true, "noUncheckedIndexedAccess": true }

/**
 * 使用 as const 对象替代字符串枚举
 * 优势:tree-shaking 友好、运行时为普通对象、与 JSON 序列化兼容
 */
const UserStatus = {
  Active: 'ACTIVE',
  Inactive: 'INACTIVE',
  Pending: 'PENDING',
  Suspended: 'SUSPENDED',
} as const;

type UserStatus = (typeof UserStatus)[keyof typeof UserStatus];
// 等价类型:'ACTIVE' | 'INACTIVE' | 'PENDING' | 'SUSPENDED'

interface User {
  id: string;
  name: string;
  status: UserStatus;
}

/**
 * 枚举值校验工具
 * 检查字符串是否为合法的 UserStatus 值
 */
function isUserStatus(value: unknown): value is UserStatus {
  return (
    typeof value === 'string' &&
    Object.values(UserStatus).includes(value as UserStatus)
  );
}

/**
 * 从字符串安全转换为 UserStatus
 */
function parseUserStatus(value: string): UserStatus | null {
  return isUserStatus(value) ? value : null;
}

// 测试用例
const status = parseUserStatus('ACTIVE');
if (status !== null) {
  const user: User = { id: 'u-1', name: 'Alice', status };
  console.log(user);
}

// 遍历所有枚举值
Object.entries(UserStatus).forEach(([key, value]) => {
  console.log(`${key} -> ${value}`);
});

4.7 完整的 tsconfig.json 配置

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

4.8 枚举合并

/**
 * 枚举合并 - 跨文件扩展枚举
 * 警告:所有合并声明必须常量值或字面量初始化
 */

// declaration.ts
enum Weekday {
  Monday = 0,
  Tuesday = 1,
  Wednesday = 2,
}

// extension.ts(同一项目中)
enum Weekday {
  Thursday = 3,
  Friday = 4,
  Saturday = 5,
  Sunday = 6,
}

// 合并后:Weekday 包含全部 7 个成员
const today: Weekday = Weekday.Saturday;

4.9 工具函数:类型安全的枚举访问

// TS 5.4
// 通用 as const 枚举工具集

/**
 * as const 枚举的基础形状
 */
type ConstEnum = Readonly<Record<string, string | number>>;

/**
 * 提取枚举值的联合类型
 */
type EnumValues<E extends ConstEnum> = E[keyof E];

/**
 * 提取枚举键的联合类型
 */
type EnumKeys<E extends ConstEnum> = keyof E;

/**
 * 构建增强枚举对象
 * 在 as const 对象的基础上添加 values() / keys() / entries() / isValue() 方法
 */
function makeEnum<const E extends ConstEnum>(enumObj: E) {
  return {
    ...enumObj,
    values(): EnumValues<E>[] {
      return Object.values(enumObj) as EnumValues<E>[];
    },
    keys(): EnumKeys<E>[] {
      return Object.keys(enumObj) as EnumKeys<E>[];
    },
    entries(): Array<[EnumKeys<E>, EnumValues<E>]> {
      return Object.entries(enumObj) as Array<[EnumKeys<E>, EnumValues<E>]>;
    },
    isValue(value: unknown): value is EnumValues<E> {
      return (
        (typeof value === 'string' || typeof value === 'number') &&
        this.values().includes(value as EnumValues<E>)
      );
    },
    fromValue(value: EnumValues<E>): EnumKeys<E> | null {
      const entry = this.entries().find(([, v]) => v === value);
      return entry ? entry[0] : null;
    },
  };
}

// 使用示例
const Color = makeEnum({
  Red: '#FF0000',
  Green: '#00FF00',
  Blue: '#0000FF',
} as const);

type Color = ReturnType<typeof Color.values>[number];
// '#FF0000' | '#00FF00' | '#0000FF'

Color.values();         // ['#FF0000', '#00FF00', '#0000FF']
Color.keys();           // ['Red', 'Green', 'Blue']
Color.entries();        // [['Red', '#FF0000'], ...]
Color.isValue('#FFF');  // false
Color.fromValue('#FF0000');  // 'Red'

4.10 状态机实现

// TS 5.4 - 基于枚举的有限状态机

const OrderState = {
  Pending: 'PENDING',
  Paid: 'PAID',
  Shipped: 'SHIPPED',
  Delivered: 'DELIVERED',
  Cancelled: 'CANCELLED',
  Refunded: 'REFUNDED',
} as const;

type OrderState = (typeof OrderState)[keyof typeof OrderState];

/**
 * 状态转移图 - 使用条件类型保证非法转移在编译期被拒绝
 */
type TransitionMap = {
  [OrderState.Pending]: OrderState.Paid | OrderState.Cancelled;
  [OrderState.Paid]: OrderState.Shipped | OrderState.Refunded;
  [OrderState.Shipped]: OrderState.Delivered;
  [OrderState.Delivered]: OrderState.Refunded;
  [OrderState.Cancelled]: never;
  [OrderState.Refunded]: never;
};

class OrderStateMachine<S extends OrderState> {
  constructor(private state: S) {}

  /**
   * 执行状态转移
   * 类型系统保证只能转移到 TransitionMap 中允许的状态
   */
  transition<N extends TransitionMap[S]>(next: N): OrderStateMachine<N> {
    console.log(`Transition: ${this.state} -> ${next}`);
    return new OrderStateMachine(next);
  }

  getState(): S {
    return this.state;
  }
}

// 使用:编译期校验状态转移合法性
const order = new OrderStateMachine(OrderState.Pending)
  .transition(OrderState.Paid)         // OK: Pending -> Paid
  .transition(OrderState.Shipped)      // OK: Paid -> Shipped
  .transition(OrderState.Delivered);   // OK: Shipped -> Delivered

// 以下代码会在编译期报错:
// order.transition(OrderState.Pending);  // Error: Delivered 不能转移到 Pending

5. 对比分析

5.1 与其他语言的枚举对比

语言枚举类型底层类型反向映射和类型支持方法支持Tree-shaking 友好
TypeScript enum命名常量集合number/string数字枚举支持否(仅判别字段)否否(运行时对象)
TypeScript as const冻结对象 + 联合类型任意字面量需手动实现是通过工厂函数是
Java enum类(继承 java.lang.Enum)int (默认)valueOf()否是N/A
C# enum值类型任意整型Enum.GetName()否否N/A
Rust enum代数数据类型标签 + 载荷通过 match是(核心特性)通过 impl是
Haskell data代数数据类型标签 + 载荷通过模式匹配是(核心特性)通过 typeclass是
Python Enum类(元类 EnumMeta)任意__members__否是N/A
Flow字符串/数字字面量联合字面量不支持否否是
Goiota 常量 + 自定义类型int不支持否否是

5.2 与 Flow 的对比

Flow 不提供 enum 关键字,推荐使用字面量联合类型:

// Flow
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';

// TypeScript 等价物(as const 模式)
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];

Flow 的方案更轻量但不便于遍历,TypeScript as const 兼顾了类型安全与运行时可访问性。

5.3 与 Python type hints 的对比

Python 3.4 引入 enum 模块,3.5+ 通过 Literal 类型支持字面量类型:

# Python
from enum import Enum, auto
from typing import Literal

class Direction(Enum):
    UP = auto()
    DOWN = auto()

# 现代替代:Literal
DirectionLiteral = Literal['UP', 'DOWN']

对比 TypeScript:

特性Python EnumTypeScript enumTypeScript as const
运行时存在是(类实例)是(对象)是(冻结对象)
类型安全中等强强
方法支持是否通过工厂
序列化需自定义字符串枚举原生JSON 兼容
Tree-shaking不适用不友好友好

5.4 与 Rust enum 的对比

Rust 的 enum 是真正的和类型,可携带载荷:

// Rust
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

fn area(s: Shape) -> f64 {
    match s {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
    }
}

TypeScript 中等效实现需要使用可区分联合:

// TypeScript
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle':
      return Math.PI * s.radius ** 2;
    case 'rectangle':
      return s.width * s.height;
  }
}

TypeScript 可区分联合在表达能力上接近 Rust enum,但缺少:

  • 穷尽性检查:TypeScript 在 switch 中需要手动添加 default 分支或启用 noImplicitReturns
  • 方法绑定:Rust 可通过 impl Shape,TypeScript 需在函数外定义
  • 零成本抽象:Rust 编译为标签 + 载荷,TypeScript 编译为对象

5.5 与 Haskell data 的对比

Haskell 的代数数据类型是和类型 + 积类型的组合:

-- Haskell
data Shape = Circle Double | Rectangle Double Double

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h

TypeScript 表达等价语义的方式与 Rust 类似(可区分联合),但缺少:

  • 模式匹配:Haskell 的模式匹配是完整语言特性,TypeScript 仅通过 switch + 控制流分析近似
  • 类型类:Haskell 的 deriving (Eq, Ord, Show) 自动派生,TypeScript 需手动实现
  • 不可变性:Haskell 数据天然不可变,TypeScript 需通过 readonly 与 as const 显式标注

6. 常见陷阱与最佳实践

6.1 陷阱一:数字枚举的反向映射破坏对象遍历

// 反面示例
enum Direction { Up, Down, Left, Right }

// 遍历 Direction 会得到 8 个属性(4 正向 + 4 反向)
Object.keys(Direction).forEach(k => console.log(k));
// '0', '1', '2', '3', 'Up', 'Down', 'Left', 'Right'

最佳实践:使用 as const 对象,遍历行为符合直觉:

const Direction = { Up: 0, Down: 1, Left: 2, Right: 3 } as const;
Object.keys(Direction);   // ['Up', 'Down', 'Left', 'Right']
Object.values(Direction); // [0, 1, 2, 3]

6.2 陷阱二:const enum 在 isolatedModules 下的不一致行为

// 跨文件使用 const enum
// file1.ts
export const enum Color { Red = '#FF0000' }

// file2.ts
import { Color } from './file1';
const c = Color.Red;

问题:

  • tsc 直接编译:内联为 '#FF0000'
  • Babel、esbuild、swc:报错 Cannot read property 'Red' of undefined(不支持跨文件 const enum 内联)
  • Vite、Next.js 默认使用 esbuild,会导致运行时错误

最佳实践:

  • 应用内部代码可使用 const enum
  • 发布到 npm 的库禁止使用 const enum
  • 在 tsconfig.json 中设置 "isolatedModules": true 提前发现问题

6.3 陷阱三:枚举与字面量联合的结构不兼容

enum Status { Active = 'ACTIVE', Inactive = 'INACTIVE' }
type StatusUnion = 'ACTIVE' | 'INACTIVE';

// 报错:枚举与字面量联合不可互换
const s1: Status = 'ACTIVE';            // Error
const s2: StatusUnion = Status.Active;  // Error

// 必须显式转换
const s3: Status = 'ACTIVE' as Status;

最佳实践:在 API 边界保持一致——要么全部使用 enum,要么全部使用 as const。混合使用会引入大量类型断言。

6.4 陷阱四:异构枚举的语义混淆

// 反面示例
enum Mixed {
  No = 0,
  Yes = 'YES',
}

// 类型推断混乱
const x: Mixed = Mixed.No;       // 0
const y: Mixed = Mixed.Yes;      // 'YES'
// Mixed 类型同时是 number 与 string 的子类型

最佳实践:禁止使用异构枚举。将其拆分为独立的数字枚举与字符串枚举,或在 ESLint 中启用 @typescript-eslint/no-mixed-enums(待提案)规则。

6.5 陷阱五:枚举的 keyof typeof 行为

enum Status { Active, Inactive }

type Keys1 = keyof typeof Status;
// 'Active' | 'Inactive'

type Keys2 = keyof Status;
// never(因为 Status 类型本身只有 number 索引签名)

// 正确做法:始终使用 keyof typeof

6.6 陷阱六:枚举值重复的反向映射覆盖

enum E {
  A = 1,
  B = 1,
}

console.log(E[1]); // 'B'('A' 被覆盖)
console.log(E.A);  // 1
console.log(E.B);  // 1

最佳实践:开启 ESLint 规则 no-duplicate-case,并使用 as const 对象,重复值会在字面量类型层面立即报错。

6.7 陷阱七:枚举在 JSON 序列化中的语义

enum Status { Active = 'ACTIVE' }

const obj = { status: Status.Active };
const json = JSON.stringify(obj);
// '{"status":"ACTIVE"}'

// 反序列化时无法自动还原为枚举
const parsed = JSON.parse(json);
console.log(parsed.status === Status.Active); // true(字符串枚举)
// 但类型层 status 是 any,需手动校验

最佳实践:在 API 边界使用 zod、io-ts、arktype 等 schema 校验库进行运行时验证:

import { z } from 'zod';

const StatusSchema = z.enum(['ACTIVE', 'INACTIVE']);
type Status = z.infer<typeof StatusSchema>;

function parseStatus(raw: unknown): Status {
  return StatusSchema.parse(raw);
}

6.8 陷阱八:tree-shaking 下的运行时副作用

// 库代码
export enum LogLevel { Debug, Info, Warn, Error }

// 应用代码
import { LogLevel } from 'my-lib';
const level = LogLevel.Info;

问题:即使应用只使用 LogLevel.Info,打包器无法 tree-shake 掉 LogLevel 对象(因为反向映射依赖整个对象初始化)。

最佳实践:库代码使用 as const 对象,使打包器能 tree-shake 未使用的属性。

6.9 最佳实践速查表

场景推荐方案理由
应用内部状态常量as const 对象tree-shaking 友好、运行时可遍历
发布到 npm 的库as const 对象避免跨编译器不一致问题
需要编译期内联的性能关键代码const enum(仅应用内)零运行时成本
与 C#/Java 团队协作的旧项目字符串 enum心智模型一致
可区分联合判别字段字面量联合类型与 Rust/Haskell 模式匹配语义对齐
API 边界类型校验zod + 字面量联合运行时校验与静态类型一致

7. 工程实践

7.1 构建配置

// tsconfig.json - 推荐配置(TS 5.4)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true
  }
}

7.2 ESLint 配置

// .eslintrc.cjs
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'import'],
  rules: {
    // 禁止异构枚举
    '@typescript-eslint/no-mixed-enums': 'error',
    // 禁止 const enum(库代码)
    '@typescript-eslint/no-const-enum': 'error',
    // 鼓励 as const 对象替代字符串枚举
    '@typescript-eslint/prefer-as-const': 'warn',
    // 强制枚举成员命名规范
    '@typescript-eslint/naming-convention': [
      'error',
      {
        selector: 'enumMember',
        format: ['PascalCase'],
      },
    ],
  },
};

7.3 编译与类型检查

# 仅类型检查(不生成产物)
npx tsc --noEmit

# 监听模式
npx tsc --noEmit --watch

# 性能分析
npx tsc --noEmit --extendedDiagnostics

# 关键指标:
# Files:                          324
# Lines of Library:               45000
# Lines of TypeScript:            12000
# Identifiers:                    8500
# Symbols:                        18500
# Types:                          42000
# Instantiations:                 125000   <- 关注此项(递归类型易爆栈)
# Memory used:                    180MB
# Check time:                     2.3sec

7.4 调试技巧

7.4.1 查看编译产物

# 单文件编译并输出到 stdout
npx tsc --noEmit --declaration --emitDeclarationOnly false \
        --outFile /dev/stdout src/enums.ts

7.4.2 查看类型推断结果

// 使用内置工具查看类型
type ShowType<T> = { [K in keyof T]: T[K] };

// 在 IDE 中悬停查看
type T1 = typeof Direction;        // 查看 as const 对象的精确类型
type T2 = (typeof Direction)[keyof typeof Direction];  // 查看联合类型

7.4.3 使用 TypeScript Playground

7.4.4 VS Code 类型可视化

在 VS Code 中:

  1. 安装 “TypeScript Explorer” 扩展
  2. 打开任意 .ts 文件
  3. 右键枚举 → “Go to Type Definition”
  4. 在 “TypeScript” 输出面板查看完整的类型展开

7.5 性能优化

7.5.1 避免大型 as const 对象

// 反面示例:1000+ 成员的枚举
const LargeEnum = { /* ... 1000 个成员 ... */ } as const;
type LargeEnum = (typeof LargeEnum)[keyof typeof LargeEnum];

// 性能影响:
// - tsc 类型检查时间线性增长
// - 编译产物体积爆炸
// - IDE 自动补全延迟

// 替代方案:分模块声明
const Module1 = { /* ... */ } as const;
const Module2 = { /* ... */ as const;
type LargeEnum = (typeof Module1)[keyof typeof Module1]
               | (typeof Module2)[keyof typeof Module2];

7.5.2 递归深度的限制

TypeScript 类型系统对递归深度有约 1000 层的硬限制。在使用递归工具类型操作枚举时需注意:

// 安全:浅层操作
type Values<E> = E[keyof E];

// 危险:深层递归可能触发限制
type DeepValues<E> = E extends object
  ? E extends Record<string, infer V>
    ? V extends object ? DeepValues<V> : V
    : never
  : E;

7.6 测试策略

// 使用 vitest 进行枚举测试
import { describe, it, expect } from 'vitest';
import { UserStatus, isUserStatus } from './enums';

describe('UserStatus', () => {
  it('应包含所有预期成员', () => {
    expect(Object.keys(UserStatus).sort()).toEqual(
      ['Active', 'Inactive', 'Pending', 'Suspended'].sort()
    );
  });

  it('isUserStatus 应正确识别合法值', () => {
    expect(isUserStatus('ACTIVE')).toBe(true);
    expect(isUserStatus('UNKNOWN')).toBe(false);
    expect(isUserStatus(null)).toBe(false);
    expect(isUserStatus(undefined)).toBe(false);
  });

  it('每个成员的值应唯一', () => {
    const values = Object.values(UserStatus);
    const unique = new Set(values);
    expect(values.length).toBe(unique.size);
  });
});

8. 案例研究

8.1 案例一:VS Code 的枚举使用

VS Code(https://github.com/microsoft/vscode)作为 TypeScript 旗舰项目,其枚举使用模式具有参考价值。

统计(基于 2024 年 12 月主分支):

  • 全代码库约 3500 个 enum 声明
  • 其中字符串枚举占 62%,数字枚举占 35%,const enum 占 3%
  • 异构枚举为 0(团队规范禁止)

典型模式:

// vscode/src/vs/base/common/event.ts
export enum EventDeliveryQueue {
  Default,
  Private,
}

设计决策:VS Code 选择保留 enum 而非迁移到 as const,原因:

  1. 历史包袱:项目始于 2015 年,as const 尚未出现
  2. 运行时反射:某些场景需要通过 Object.keys() 反射枚举成员
  3. 团队心智模型:Microsoft 团队对 enum 语义最熟悉

8.2 案例二:Slack 的枚举迁移

Slack 桌面客户端在 2021 年完成从 enum 到 as const 的全面迁移(详见 [Slack Engineering Blog, 2021])。

迁移动机:

  • Tree-shaking 后 bundle 体积减少 4.2%
  • 跨平台编译(Babel + esbuild)一致性问题消除
  • TypeScript 4.5 后 isolatedModules 成为默认推荐

迁移策略:

  1. 编写 codemod(基于 ts-morph)自动转换字符串枚举
  2. 数字枚举保留(涉及反向映射的业务逻辑)
  3. 渐进式迁移,分 6 个月完成

8.3 案例三:Airbnb 风格指南

Airbnb TypeScript Style Guide(2024 版)明确推荐:

Prefer as const objects over enum for new code.

Rationale:

  • Better tree-shaking
  • Consistent behavior across transpilers
  • JSON-serialization friendly
  • Aligns with TypeScript 5.x direction

唯一例外:与原生库(如 Node.js fs 模块)互操作时,保留 enum 以匹配 API。

8.4 案例四:Google 的 TypeScript 风格指南

Google TypeScript Style Guide(§5.2.5):

Use enum for closed sets of values that are used in multiple places.

Use as const objects for open or extensible sets.

Google 采取折中策略:

  • 闭集(如 HTTP 状态码、星期几):使用 enum
  • 开集(如用户角色、配置选项):使用 as const
  • 核心库(Angular、gRPC):使用 enum 以兼容旧版本 TypeScript

8.5 案例五:React 项目中的状态枚举

// React + TypeScript 状态管理
import { useState, useCallback } from 'react';

const AsyncStatus = {
  Idle: 'IDLE',
  Pending: 'PENDING',
  Fulfilled: 'FULFILLED',
  Rejected: 'REJECTED',
} as const;

type AsyncStatus = (typeof AsyncStatus)[keyof typeof AsyncStatus];

interface AsyncState<T> {
  status: AsyncStatus;
  data: T | null;
  error: Error | null;
}

function useAsync<T>(fn: () => Promise<T>) {
  const [state, setState] = useState<AsyncState<T>>({
    status: AsyncStatus.Idle,
    data: null,
    error: null,
  });

  const execute = useCallback(async () => {
    setState({ status: AsyncStatus.Pending, data: null, error: null });
    try {
      const data = await fn();
      setState({ status: AsyncStatus.Fulfilled, data, error: null });
    } catch (error) {
      setState({
        status: AsyncStatus.Rejected,
        data: null,
        error: error instanceof Error ? error : new Error(String(error)),
      });
    }
  }, [fn]);

  return { ...state, execute };
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  const { status, data, error, execute } = useAsync(
    () => fetch(`/api/users/${userId}`).then(r => r.json())
  );

  if (status === AsyncStatus.Pending) return <div>Loading...</div>;
  if (status === AsyncStatus.Rejected) return <div>Error: {error?.message}</div>;
  if (status === AsyncStatus.Fulfilled && data) {
    return <div>{data.name}</div>;
  }
  return <button onClick={execute}>Load</button>;
}

填空题知识点讲解

题目 1:TypeScript 引入 as const 断言的版本是 _______。

解析讲解:3.4

解析讲解:TypeScript 3.4(2019 年 3 月发布)引入 as const 断言,是现代枚举替代方案的基石。


题目 2:数字枚举的反向映射可形式化为函数 f:Names↔Numbersf: \text{Names} \leftrightarrow \text{Numbers},其性质为 _______。

解析讲解:双射(bijection)/ 单射(injection)

解析讲解:当枚举值唯一时,ff 是双射;若存在重复值,ff 退化为单射(值到名称的映射会丢失信息)。


题目 3:const enum Color { Red = '#FF0000' } 编译后的 JavaScript 产物大小为 _______ 个语句。

解析讲解:0

解析讲解:const enum 在编译期被完全内联,不产生任何运行时对象或语句。所有引用点被替换为字面量值。


题目 4:使用 as const 对象 + 联合类型替代字符串枚举时,类型表达式 (typeof Obj)[keyof typeof Obj] 的作用是 _______。

解析讲解:提取对象所有值的字面量联合类型

解析讲解:typeof Obj 获取对象的精确类型(含字面量),keyof typeof Obj 获取所有键的联合,[keyof typeof Obj] 通过索引访问提取所有值的联合类型。

编程题知识点讲解

题目 1:实现一个类型安全的枚举工具函数 enumFromObject,输入一个 as const 对象,返回增强的枚举对象(含 values()、keys()、isValue() 方法)。

解析讲解:

// TS 5.4
type ConstEnum = Readonly<Record<string, string | number>>;

function enumFromObject<const E extends ConstEnum>(enumObj: E) {
  const values = Object.values(enumObj) as Array<E[keyof E]>;
  const entries = Object.entries(enumObj) as Array<[keyof E, E[keyof E]]>;

  return {
    ...enumObj,
    values(): E[keyof E][] {
      return [...values];
    },
    keys(): (keyof E)[] {
      return entries.map(([k]) => k);
    },
    entries(): Array<[keyof E, E[keyof E]]> {
      return entries.map(([k, v]) => [k, v]);
    },
    isValue(value: unknown): value is E[keyof E] {
      return (
        (typeof value === 'string' || typeof value === 'number') &&
        values.includes(value as E[keyof E])
      );
    },
    fromValue(value: E[keyof E]): keyof E | null {
      return entries.find(([, v]) => v === value)?.[0] ?? null;
    },
  };
}

// 使用
const Color = enumFromObject({
  Red: '#FF0000',
  Green: '#00FF00',
  Blue: '#0000FF',
} as const);

console.log(Color.values());           // ['#FF0000', '#00FF00', '#0000FF']
console.log(Color.keys());             // ['Red', 'Green', 'Blue']
console.log(Color.isValue('#FF0000')); // true
console.log(Color.fromValue('#FF0000')); // 'Red'

评分标准:

  • 类型推断正确(10 分)
  • 方法实现完整(10 分)
  • 运行时性能合理(5 分)

题目 2:实现一个状态机类型,使用条件类型保证非法转移在编译期被拒绝。

解析讲解:

// TS 5.4
const OrderState = {
  Pending: 'PENDING',
  Paid: 'PAID',
  Shipped: 'SHIPPED',
  Delivered: 'DELIVERED',
  Cancelled: 'CANCELLED',
} as const;

type OrderState = (typeof OrderState)[keyof typeof OrderState];

type Transitions = {
  [OrderState.Pending]: OrderState.Paid | OrderState.Cancelled;
  [OrderState.Paid]: OrderState.Shipped | OrderState.Refunded;
  [OrderState.Shipped]: OrderState.Delivered;
  [OrderState.Delivered]: OrderState.Refunded;
  [OrderState.Cancelled]: never;
  [OrderState.Refunded]: never;
};

class StateMachine<S extends OrderState> {
  constructor(private state: S) {}

  transition<N extends Transitions[S]>(next: N): StateMachine<N> {
    return new StateMachine(next);
  }

  current(): S {
    return this.state;
  }
}

// 测试
const m1 = new StateMachine(OrderState.Pending)
  .transition(OrderState.Paid)
  .transition(OrderState.Shipped)
  .transition(OrderState.Delivered);

// 以下代码编译期报错:
// new StateMachine(OrderState.Pending).transition(OrderState.Delivered);
// Error: 'DELIVERED' 不能赋值给 'PAID' | 'CANCELLED'

评分标准:

  • 状态类型定义(10 分)
  • 转移图类型正确(10 分)
  • 编译期校验有效(10 分)

10.1 学术论文

Bierman, G., Abadi, M., & Torgersen, M. (2014). Understanding TypeScript. In Proceedings of the 28th European Conference on Object-Oriented Programming (ECOOP 2014) (pp. 257–281). Springer. https://doi.org/10.1007/978-3-662-44202-9_11

Bierman, G., Parkinson, M., & Pitts, A. (2003). The effect of structural typing. In Proceedings of the 2003 ACM SIGPLAN Workshop on Mechanized Reasoning about Languages with Variable Binding (pp. 1–10). ACM. https://doi.org/10.1145/976571.976572

Hejlsberg, A. (2012). TypeScript: Static typing for JavaScript. Microsoft Research Talk. https://research.microsoft.com/apps/video/dl.aspx?id=171571

Pierce, B. C. (2002). Types and programming languages. MIT Press. ISBN: 978-0262162098.

Swamy, N., Hicks, M., & Bierman, G. (2014). Gradual typing for JavaScript. In Proceedings of the 29th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA 2014) (pp. 1–27). ACM. https://doi.org/10.1145/2660193.2660232

10.2 官方文档与规范

TypeScript Language Specification (2014). Microsoft. https://github.com/microsoft/TypeScript/blob/main/doc/spec-ARCHIVED.md

TypeScript Handbook: Enums (2024). Microsoft. https://www.typescriptlang.org/docs/handbook/enums.html

ECMAScript 2024 Language Specification (2024). ECMA International. ECMA-262, 14th edition. https://tc39.es/ecma262/

TC39 Proposal: Enums (Stage 1). (2023). https://github.com/Jack-Works/proposal-enum

10.3 工程实践文献

Airbnb. (2024). TypeScript style guide. https://github.com/airbnb/typescript

Google. (2024). Google TypeScript style guide. https://google.github.io/styleguide/tsguide.html

Microsoft. (2024). TypeScript 5.4 release notes. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html

Rosenwasser, D. (2022). Announcing TypeScript 4.9. Microsoft DevBlog. https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/

10.4 历史资料

Hejlsberg, A. (2017). TypeScript: The first six years. GopherCon 2017 Keynote. https://www.youtube.com/watch?v=jXccn7GYn94

Bierman, G. (2012). TypeScript design notes. Microsoft Research Cambridge.


11.1 书籍

  • 《Effective TypeScript》(Dan Vanderkam,2024 第二版)- 第 6 章”类型设计”深入讨论枚举与 as const 的取舍
  • 《Programming TypeScript》(Boris Cherny,2023 第三版)- 第 4 章覆盖枚举的完整语法与陷阱
  • 《TypeScript in 50 Lessons》(Stefan Baumgartner,2024)- 第 12 课讲解 as const 在生产代码中的应用
  • 《Learning TypeScript》(Josh Goldberg,2022)- 第 5 章枚举与现代替代方案对比
  • 《Type-Driven Development with Idris》(Edwin Brady,2017)- 类型驱动开发的奠基之作,可借鉴其代数数据类型思想到 TypeScript

11.3 论文与演讲

  • “TypeScript: The first six years”(Anders Hejlsberg, GopherCon 2017)- TypeScript 设计动机的一手资料
  • “Understanding TypeScript”(Gavin Bierman, ECOOP 2014)- TypeScript 类型系统的形式化分析
  • “Gradual Typing for JavaScript”(Swamy et al., OOPSLA 2014)- 渐进式类型系统的理论基础
  • “Type Classes: Exploring the Design Space”(Wadler & Blott, 1989)- Haskell typeclass 设计,可借鉴到 TypeScript 工具类型

11.4 相关开源项目

11.5 进阶主题

完成本章学习后,建议继续探索:

  1. 模板字面量类型(TypeScript 4.1+):基于 as const 实现键名自动生成
  2. 条件类型分发:理解枚举在分布式条件类型中的行为
  3. 映射类型与键重映射:使用 as 子句转换 as const 对象的键
  4. 装饰器标准实现:使用装饰器声明式校验枚举值
  5. 声明文件编写:为 C/C++ 原生枚举编写 .d.ts 类型声明

附录 A:本章代码示例的 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}

附录 B:术语表

术语英文含义
枚举enum命名常量集合
数字枚举numeric enum底层为 number 的枚举
字符串枚举string enum底层为 string 的枚举
异构枚举heterogeneous enum混合 number 与 string 的枚举(不推荐)
const enumconst enum编译期内联的枚举
反向映射reverse mapping数字枚举的值到名称的映射
可区分联合discriminated union通过判别字段收窄的联合类型
和类型sum typeRust/Haskell 中可携带载荷的枚举
as const 断言as const assertion将对象收窄为字面量类型的断言
字面量类型literal type具体值的类型(如 'ACTIVE'、42)

数字枚举

换行写法:定义数字枚举 enum <枚举名> { <成员1>, <成员2>, }

// 定义数字枚举(自动从 0 开始递增)
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

换行写法:指定起始值的数字枚举 enum <枚举名> { <成员1> = <值>, <成员2>, }

// 指定起始值的数字枚举
enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

换行写法:指定每个成员的值 enum <枚举名> { <成员1> = <值1>, <成员2> = <值2>, }

// 指定每个成员的值
enum StatusCode {
    OK = 200,
    NotFound = 404,
    ServerError = 500,
}

基本写法:访问数字枚举成员 <枚举名>.<成员>

// 访问数字枚举成员
let direction: Direction = Direction.Up

基本写法:反向映射(数字枚举) <枚举名>[<数字>]

// 数字枚举的反向映射
let name: string = Direction[0]  // "Up"

字符串枚举

换行写法:定义字符串枚举 enum <枚举名> { <成员1> = "<值1>", <成员2> = "<值2>", }

// 定义字符串枚举
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

基本写法:访问字符串枚举成员 <枚举名>.<成员>

// 访问字符串枚举成员
let color: Color = Color.Red

基本写法:获取字符串枚举的值 <枚举名>.<成员>

// 获取字符串枚举的值
let value: string = Color.Red  // "RED"

异构枚举

换行写法:混合数字和字符串枚举 enum <枚举名> { <成员1> = <数字>, <成员2> = "<字符串>", }

// 混合数字和字符串的异构枚举
enum Boolean {
    No = 0,
    Yes = "YES",
}

常量枚举

换行写法:定义常量枚举 const enum <枚举名> { <成员1>, <成员2>, }

// 定义常量枚举(编译时内联,不生成代码)
const enum Direction {
    Up,
    Down,
    Left,
    Right,
}

基本写法:使用常量枚举 let <变量>: <枚举名> = <枚举名>.<成员>

// 使用常量枚举(编译时替换为具体值)
let direction: Direction = Direction.Up

枚举与联合类型

换行写法:从枚举提取联合类型 type <类型> = \${<枚举名>}“

// 从枚举提取字符串联合类型
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

type ColorValue = `${Color}`  // "RED" | "GREEN" | "BLUE"

基本写法:枚举成员作为类型 type <类型> = <枚举名>.<成员>

// 枚举成员作为类型
type RedColor = Color.Red

枚举与映射类型

换行写法:枚举键映射 type <类型> = { [P in <枚举名>]: <类型> }

// 从枚举创建映射类型
enum Status {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
}

type StatusMessages = {
    [P in Status]: string
}

换行写法:枚举值映射 type <类型> = { [P in <枚举名>]: <类型> }

// 从枚举创建值映射类型
type StatusConfig = {
    [P in Status]: {
        label: string
        color: string
    }
}

枚举与条件类型

换行写法:枚举条件类型 type <类型> = <T> extends <枚举名> ? <真类型> : <假类型>

// 枚举条件类型
type IsColor<T> = T extends Color ? true : false

枚举方法

换行写法:枚举与命名空间合并 enum <枚举名> { <成员> } namespace <枚举名> { export function <方法>(<参数>): <返回类型> { <语句> } }

// 枚举与命名空间合并(为枚举添加方法)
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

namespace Color {
    export function from_string(value: string): Color | undefined {
        switch (value) {
            case "RED": return Color.Red
            case "GREEN": return Color.Green
            case "BLUE": return Color.Blue
            default: return undefined
        }
    }
}

基本写法:调用枚举方法 <枚举名>.<方法>(<参数>)

// 调用枚举方法
let color = Color.from_string("RED")

枚举与对象

换行写法:使用对象替代枚举 const <对象> = { <成员1>: "<值1>", <成员2>: "<值2>", } as const

// 使用对象替代枚举(使用 as const)
const Color = {
    Red: "RED",
    Green: "GREEN",
    Blue: "BLUE",
} as const

基本写法:从对象提取类型 type <类型> = typeof <对象>[keyof typeof <对象>]

// 从对象提取联合类型
type ColorValue = typeof Color[keyof typeof Color]  // "RED" | "GREEN" | "BLUE"

枚举与 switch

换行写法:枚举与 switch 语句 function <函数>(<参数>: <枚举名>): <返回类型> { switch (<参数>) { case <枚举名>.<成员1>: return <处理1> case <枚举名>.<成员2>: return <处理2> } }

// 枚举与 switch 语句
function get_color_name(color: Color): string {
    switch (color) {
        case Color.Red:
            return "红色"
        case Color.Green:
            return "绿色"
        case Color.Blue:
            return "蓝色"
    }
}

枚举与穷尽检查

换行写法:使用 never 进行穷尽检查 function <函数>(<参数>: <枚举名>): <返回类型> { switch (<参数>) { case <枚举名>.<成员1>: return <处理1> case <枚举名>.<成员2>: return <处理2> default: const _exhaustive: never = <参数> return _exhaustive } }

// 使用 never 进行穷尽检查
function get_color_name(color: Color): string {
    switch (color) {
        case Color.Red:
            return "红色"
        case Color.Green:
            return "绿色"
        case Color.Blue:
            return "蓝色"
        default:
            const _exhaustive: never = color
            return _exhaustive
    }
}

枚举与 const 断言

换行写法:使用 const 断言替代枚举 const <对象> = { <成员1>: <值1>, <成员2>: <值2>, } as const type <类型> = keyof typeof <对象>

// 使用 const 断言替代枚举
const Direction = {
    Up: "UP",
    Down: "DOWN",
    Left: "LEFT",
    Right: "RIGHT",
} as const

type Direction = keyof typeof Direction  // "Up" | "Down" | "Left" | "Right"

基本写法:使用 const 断言对象 let <变量>: <类型> = "<值>"

// 使用 const 断言对象
let direction: Direction = "Up"

枚举与映射

换行写法:枚举值映射 const <映射>: Record<<枚举名>, <类型>> = { [<枚举名>.<成员1>]: <值1>, [<枚举名>.<成员2>]: <值2>, }

// 枚举值映射
const ColorHex: Record<Color, string> = {
    [Color.Red]: "#FF0000",
    [Color.Green]: "#00FF00",
    [Color.Blue]: "#0000FF",
}

基本写法:访问枚举映射 <映射>[<枚举名>.<成员>]

// 访问枚举映射
let hex: string = ColorHex[Color.Red]

枚举与类型守卫

换行写法:枚举类型守卫 function <函数>(<参数>: any): <参数> is <枚举名> { return Object.values(<枚举名>).includes(<参数>) }

// 枚举类型守卫
function is_color(value: any): value is Color {
    return Object.values(Color).includes(value)
}

基本写法:使用枚举类型守卫 if (<函数>(<值>)) { <语句> }

// 使用枚举类型守卫
let value: any = "RED"
if (is_color(value)) {
    let color: Color = value
}

枚举与反向映射

换行写法:字符串枚举反向映射 const <映射>: Record<string, <枚举名>> = { ["<值1>"]: <枚举名>.<成员1>, ["<值2>"]: <枚举名>.<成员2>, }

// 字符串枚举反向映射
const ColorFromValue: Record<string, Color> = {
    ["RED"]: Color.Red,
    ["GREEN"]: Color.Green,
    ["BLUE"]: Color.Blue,
}

基本写法:使用反向映射 let <变量>: <枚举名> = <映射>["<值>"]

// 使用反向映射
let color: Color = ColorFromValue["RED"]

枚举与迭代

换行写法:迭代枚举值 for (const <值> of Object.values(<枚举名>)) { <语句> }

// 迭代枚举值
for (const color of Object.values(Color)) {
    console.log(color)
}

换行写法:迭代枚举键值对 for (const [<键>, <值>] of Object.entries(<枚举名>)) { <语句> }

// 迭代枚举键值对
for (const [key, value] of Object.entries(Color)) {
    console.log(`${key}: ${value}`)
}

枚举与工具类型

换行写法:获取枚举所有值 type <类型> = \${<枚举名>}“

// 获取枚举所有值的联合类型
type ColorValues = `${Color}`  // "RED" | "GREEN" | "BLUE"

换行写法:获取枚举所有键 type <类型> = keyof typeof <枚举名>

// 获取枚举所有键的联合类型
type ColorKeys = keyof typeof Color  // "Red" | "Green" | "Blue"

枚举与函数

换行写法:枚举作为函数参数 function <函数>(<参数>: <枚举名>): <返回类型> { <语句> }

// 枚举作为函数参数
function get_color_code(color: Color): string {
    return color
}

换行写法:枚举作为函数返回值 function <函数>(<参数>: <类型>): <枚举名> { <语句> }

// 枚举作为函数返回值
function parse_color(value: string): Color {
    switch (value) {
        case "RED": return Color.Red
        case "GREEN": return Color.Green
        case "BLUE": return Color.Blue
        default: throw new Error("Invalid color")
    }
}

枚举与接口

换行写法:枚举与接口组合 interface <接口> { <属性>: <枚举名> }

// 枚举与接口组合
interface User {
    name: string
    status: Status
}

换行写法:枚举与类型别名 type <类型> = { <属性>: <枚举名> }

// 枚举与类型别名组合
type Config = {
    color: Color
    direction: Direction
}