news 2026/9/12 20:41:15

Refine 实战:TypeScript Enum 完全指南——从成员初始化、编译映射到类与类型系统集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine 实战:TypeScript Enum 完全指南——从成员初始化、编译映射到类与类型系统集成

Refine 实战:TypeScript Enum 完全指南——从成员初始化、编译映射到类与类型系统集成

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

本篇技术指南以 TypeScript Enum 为核心主题,结合 Refine 开源仓库中真实使用的枚举案例(如 CLI 包中的UIFrameworks、核心包 undoableQueue 中的ActionTypes),系统讲解字符串枚举与数值枚举的定义、成员初始化规则、常量值与计算值、编译期 IIFE 转换与运行时双向/单向映射,以及如何利用枚举生成成员级子类型与键联合类型,最终把这些概念整合进一个基于分级订阅(Subscription)模型的可运行类实现中。

导读

TypeScript 的enum(枚举)是围绕一个中心主题组织的一组命名常量,它既提供类型层面的约束,又会在运行时注入真实的 JavaScript 对象。本文以"分级订阅模型"(AccountType账户类型 +BillingSchedule计费周期)为贯穿案例,从零开始定义字符串枚举与数值枚举,深入编译产物与运行时对象,讲解成员初始化、常量/计算值、方向映射、由枚举派生的子类型与键联合类型,最后实现一个完整的PersonalSubscription类。读完本文,你将能够:熟练声明两种枚举并理解其初始化规则;看懂枚举编译成的 IIFE 及其运行时对象结构;利用keyof typeof提取枚举键联合类型;在类、接口与泛型场景中组合使用枚举的类型与值两面能力。

本文案例与结论均可直接在本仓库源码中验证。例如 packages/cli/src/definitions/uiFrameworks.ts 定义了字符串枚举UIFrameworksANTD = "antd"MUI = "mui"等),packages/core/src/contexts/undoableQueue/types.ts 定义了ActionTypes枚举,而 packages/core/src/contexts/data/types.ts 中的MutationMode = "pessimistic" | "optimistic" | "undoable"则展示了与枚举对立的字符串联合类型方案,适合作为对照学习。

前置要求

要运行本文的全部示例,你需要一个可执行 TypeScript 的 JavaScript 引擎。可以是本地安装好 TypeScript 的 Node.js 环境,也可以使用 TypeScript 官方 Playground。所有示例代码均为标准 TypeScript,不依赖任何第三方库,可直接复制到.ts文件中通过tsc编译或用ts-node/tsx运行验证。

TypeScript 枚举示例:分级订阅模型

为了直观说明枚举的各种概念,我们假设存在一个存储订阅实体的subscriptions表,其中有accountType(账户类型)和billingSchedule(计费周期)两个属性:

  • accountType的取值是PersonalStartupEnterpriseCustom之一;
  • billingSchedule的取值是FreeMonthlyQuarterlyYearly之一。

这些可选项表达了按账户类型与计费周期对订阅进行分组的意图,非常适合用 TypeScriptenum来建模。使用枚举不仅能为这两个属性声明类型,还能创建出本需要从数据库表派生出来的代表性运行时对象。

首先为accountType定义枚举。这里使用字符串字面量初始化所有成员,属于字符串枚举

enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom", }

再为billingSchedule定义枚举,第一个成员用数值字面量初始化,属于数值枚举

enum BillingSchedule { FREE = 0, MONTHLY, QUARTERLY, YEARLY, }

枚举会产生运行时 JavaScript 对象

枚举不只是类型定义,还会把 JS 对象注入运行时环境。运行下面这段代码即可观察:

const accountType = AccountType.PERSONAL; const billingSchedule = BillingSchedule.FREE; console.log(accountType); // "Personal" console.log(billingSchedule); // 0

AccountType.PERSONALBillingSchedule.FREE是在运行时真正访问对象成员并拿到对应值。这说明枚举定义既提供类型层面的约束,也会向应用引入真实的 JS 对象。

仓库中同样存在这种用法:ActionTypes枚举在 packages/core/src/contexts/undoableQueue/index.tsx 的notificationDispatchreducer 中被用作case ActionTypes.ADD:case ActionTypes.REMOVE:case ActionTypes.DECREASE_NOTIFICATION_SECOND:的分支判断,枚举成员在运行时被作为可比较的常量值使用。

TypeScript 中的枚举类型

枚举成员通常用来存储常量,值可以是字符串常量、数值常量或两者的混合。成员值是否同质(homogeneous)决定了它是字符串枚举还是数值枚举。

TypeScript 字符串枚举

当枚举的所有成员都是字符串值时,它就是字符串枚举,如AccountType

enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom", }

TypeScript 数值枚举

同理,当所有成员都是数值时,枚举就是数值枚举:

enum BillingSchedule { FREE = 0, MONTHLY, QUARTERLY, YEARLY, }

这里第一个成员被初始化为数字,后续成员虽然没有显式初始化,但 TypeScript 会按1自动递增。因此所有成员都是数值,BillingSchedule是数值枚举。

TypeScript 枚举成员初始化

字符串枚举成员必须显式初始化字符串值;数值枚举成员可以保持未初始化,由 TypeScript 隐式赋值。

字符串枚举中的成员初始化

AccountType所示,字符串枚举要求显式初始化所有成员。用字符串字面量可以更明确地表达分组意图,这对应用特性和开发者体验都有帮助;显式的字符串初始化还有助于运行时 JS 对象的序列化。

字符串成员后面紧跟一个未初始化成员是非法的:

enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM, // ❌ Enum member must have initializer.(1061) }

而如果把未初始化成员放在第一位,它会被默认赋值为0,此时就得到一个与字符串成员混用的异构枚举

// Heterogenous enum(异构枚举) enum AccountType { CUSTOM, PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", } const accountTypeCustom = AccountType.CUSTOM; console.log(accountTypeCustom); // 0

数值枚举中的成员初始化

数值枚举的成员可以显式初始化,也可以不初始化。

显式初始化的数值成员

BillingSchedule中我们给第一个成员显式赋了0

// 第一个成员显式初始化,后续成员自动递增 enum BillingSchedule { FREE = 0, MONTHLY, QUARTERLY, YEARLY, }

后续成员会依次自动加1

console.log(BillingSchedule.MONTHLY); // 1 console.log(BillingSchedule.QUARTERLY); // 2 console.log(BillingSchedule.YEARLY); // 3

显式初始化某个成员相当于设定一个偏移量,后续未初始化成员的值都基于该偏移量递增。给第一个成员赋0就是零偏移。事实上完全可以不初始化任何成员,因为默认偏移量本来就是0

// 完全不初始化 enum BillingSchedule { FREE, MONTHLY, QUARTERLY, YEARLY, } console.log(BillingSchedule.FREE); // 0

偏移量可以出现在任意位置,并影响其后隐式递增的成员值:

enum BillingSchedule { FREE, MONTHLY, QUARTERLY = 5, YEARLY, } console.log(BillingSchedule.MONTHLY); // 1 console.log(BillingSchedule.QUARTERLY); // 5 console.log(BillingSchedule.YEARLY); // 6

TypeScript 枚举在编译期与运行时的表现

编译时,TypeScript 会把枚举翻译成对应的 IIFE(立即执行函数表达式),该 IIFE 再向运行时引入枚举的 JavaScript 对象表示。

字符串成员与数值成员在编译行为上不同:字符串成员只做单向映射(键 → 值),数值成员则做双向映射(键 ↔ 值)。因此字符串枚举只能通过常量名访问,数值枚举则能通过值与键双向导航。

字符串枚举的单向映射

AccountType编译后的 JS 代码大致如下:

/* enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom" } */ "use strict"; var AccountType; (function (AccountType) { AccountType.PERSONAL = "Personal"; AccountType["STARTUP"] = "Startup"; AccountType["ENTERPRISE"] = "Enterprise"; AccountType["CUSTOM"] = "Custom"; })(AccountType || (AccountType = {}));

这个 IIFE 向运行时注入的对象结构为:

{ PERSONAL: "Personal", STARTUP: "Startup", ENTERPRISE: "Enterprise", CUSTOM: "Custom" }

字符串成员的单向映射只把常量名作为键,因此只能通过常量名访问,不能通过值反查:

console.log(AccountType.PERSONAL); // "Personal" console.log(AccountType.Personal); // ❌ Property 'Personal' does not exist on type 'typeof AccountType'. Did you mean 'PERSONAL'?(2551)

数值枚举的双向映射

与字符串枚举的单向映射相反,数值枚举编译成双向 JS 对象。BillingSchedule编译后的 IIFE 大致如下:

/* enum BillingSchedule { FREE, MONTHLY, QUARTERLY = 5, YEARLY } */ "use strict"; var BillingSchedule; (function (BillingSchedule) { BillingSchedule[(BillingSchedule["FREE"] = 0)] = "FREE"; BillingSchedule[(BillingSchedule["MONTHLY"] = 1)] = "MONTHLY"; BillingSchedule[(BillingSchedule["QUARTERLY"] = 5)] = "QUARTERLY"; BillingSchedule[(BillingSchedule["YEARLY"] = 6)] = "YEARLY"; })(BillingSchedule || (BillingSchedule = {}));

注入运行时的对象结构为:

{ "0": "FREE", "1": "MONTHLY", "5": "QUARTERLY", "6": "YEARLY", "FREE": 0, "MONTHLY": 1, "QUARTERLY": 5, "YEARLY": 6 }

于是数值成员可以双向导航:

console.log(BillingSchedule.FREE); // 0 console.log(BillingSchedule[0]); // "FREE" console.log(BillingSchedule.YEARLY); // 6 console.log(BillingSchedule[6]); // "YEARLY"

枚举成员值:常量 vs 计算值

枚举成员值可以是常量,也可以是计算值

枚举成员的常量值

本文两个示例中的枚举值都是常量,但常量之间也有细微差别。AccountType的所有值都是字符串字面量,属于字面量枚举表达式

enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom", }

同理,BillingSchedule中的数值字面量0也是字面量枚举表达式:

enum BillingSchedule { FREE = 0, MONTHLY, QUARTERLY, YEARLY, }

未初始化、被隐式赋值为数值字面量的成员同样被视为常量,如上面这个版本的BillingSchedule所有成员皆是如此。字面量枚举表达式还有其他细微形式,例如引用另一个枚举成员的值;其余情况可查阅 TypeScript 官方 Enums 文档。

枚举成员的计算值

当成员的值由某个 JavaScript 表达式计算得出时,即为计算值。本文案例中没有用到,但一个基本实例长这样:

enum ABasicExample { A_BASIC_EXAMPLE = "A Basic Example".length, }

注意这里成员值来自表达式求值,编译时无法静态内联,这也是它与常量值的关键区别。

从 TypeScript 枚举派生类型

此前我们只探讨了枚举的对象面。现在来看枚举在类型层面如何发挥作用:当枚举的所有成员都是字面量枚举表达式时,每个成员都会生成对应的成员类型,枚举本身则相当于所有子类型的联合。

成员级类型(Individual Types)

当所有成员都是字符串字面量或数值字面量时,每个成员都会生成独立类型。用这些独立类型可以定义新的子类型。例如从AccountType派生出账户子类型:

type TPersonalAccount = { tier: AccountType.PERSONAL; postsQuota: number; verified: boolean; }; interface IStartupAccount { tier: AccountType.STARTUP; postsQuota: number; verified: boolean; }

上面用AccountType.PERSONALAccountType.STARTUP成员类型定义了新的账户子类型。同样地,也可以从BillingSchedule成员派生子类型:

interface IFreeBilling { tier: BillingSchedule.FREE; startDate: string | boolean; expiryDate: string | boolean; }

成员键的联合类型

枚举本身生成的类型实际上是所有成员类型的联合,可以通过keyof typeof链式获取:

/* enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom" } */ type TAccountType = keyof typeof AccountType; /* 生成的类型等价于: type TAccountType = "PERSONAL" | "STARTUP" | "ENTERPRISE" | "CUSTOM"; */

上面的代码先用typeof取到枚举对象,再用keyof取得成员名(键)。keyof typeof这种模式在需要枚举键字符串集合的场景(如表单字段名、配置项键列表)非常实用。

在类中使用 TypeScript 枚举

现在把枚举与类型定义整合起来,实现一个简单的PersonalSubscription类:

enum AccountType { PERSONAL = "Personal", STARTUP = "Startup", ENTERPRISE = "Enterprise", CUSTOM = "Custom", } enum BillingSchedule { FREE, MONTHLY, QUARTERLY, YEARLY, } type TAccount<AccountType> = { tier: AccountType; postsQuota: number; verified: boolean; }; interface IBilling<BillingSchedule> { tier: BillingSchedule; startDate: string | boolean; expiryDate: string | boolean; } class PersonalAccount implements TAccount<AccountType.PERSONAL> { tier: AccountType.PERSONAL = AccountType.PERSONAL; postsQuota = 2; verified = false; } class FreeBilling implements IBilling<BillingSchedule.FREE> { tier: BillingSchedule.FREE = BillingSchedule.FREE; startDate = false; expiryDate = false; } interface IPersonalSubscription<TAccount, IBilling> { accountType: TAccount; billingSchedule: IBilling; creditCard: string; } class PersonalSubscription implements IPersonalSubscription< TAccount<AccountType.PERSONAL>, IBilling<BillingSchedule.FREE> > { accountType = new PersonalAccount(); billingSchedule = new FreeBilling(); creditCard: string = "XXXXXXXXXXXXXXXX"; }

上面的代码中,BillingSchedule使用了全部未初始化的数值枚举:第一个成员被赋为0,后续成员依次递增1。我们用泛型把AccountTypeBillingSchedule类型分别传入TAccountIBilling,使它们在PersonalAccountFreeBilling类以及IPersonalSubscription类型中的使用更灵活——枚举成员在这里既被当作常量值使用,也被当作类型定义使用。

这种"同一个枚举名既能做类型又能做值"的双重身份,正是类实现场景下枚举相对字符串联合类型的主要便利所在。

TypeScript 枚举的实际应用

当你需要组织一组相关常量时,枚举非常有用。以下是几个真实世界的应用场景:

1. 基于角色的访问控制

构建带不同用户角色的应用时,用枚举清晰定义角色:

enum UserRole { ADMIN = "Admin", EDITOR = "Editor", VIEWER = "Viewer", } function checkAccess(role: UserRole): void { if (role === UserRole.ADMIN) { console.log("You have full access!"); } else { console.log("Limited access only."); } }

2. API 的 HTTP 方法

用枚举标准化发起 API 请求时的 HTTP 方法:

enum HttpMethod { GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE", } function makeRequest(method: HttpMethod, url: string): void { console.log(`Sending a ${method} request to ${url}`); } makeRequest(HttpMethod.POST, "/api/users");

3. 错误类型

用枚举归类错误类型,代码更清晰:

enum ErrorType { NETWORK = "Network Error", VALIDATION = "Validation Error", SERVER = "Server Error", } function logError(type: ErrorType): void { console.log(`An error occurred: ${type}`); } logError(ErrorType.NETWORK);

4. 框架/UI 库标识(Refine 中的真实案例)

在本仓库中,字符串枚举被用来集中管理一组 UI 框架标识。CLI 包的 uiFrameworks.ts 定义了:

export enum UIFrameworks { ANTD = "antd", MUI = "mui", MANTINE = "mantine", CHAKRA = "chakra-ui", }

这样一来,CLI 在引导用户选择 antd、mui、mantine 或 chakra-ui 时,就可以用UIFrameworks作为统一常量,避免魔法字符串散落各处。核心包 undoableQueue 的 types.ts 则用ActionTypesADDREMOVEDECREASE_NOTIFICATION_SECOND)作为 reducer 的 action 标识,与"ADD"这类裸字符串相比,拼写错误会在编译期就被拦截。

枚举让代码更可读,也避免使用非法值——从此告别散落各处的魔法字符串与魔法数字。

使用枚举的常见陷阱

以下是实践中容易踩到的坑及规避方法:

忘记初始化字符串枚举

字符串枚举的所有成员必须带初始值,这一点很容易被遗漏:

// ❌ 错误 enum Color { RED, GREEN, // Error: Must have an initializer } // ✅ 正确 enum Color { RED = "Red", GREEN = "Green", }

硬编码值

早期习惯硬编码"Admin"0之类的值,维护起来非常痛苦:

// ❌ 不要这样: const userRole = "Admin"; // ✅ 应该这样: enum UserRole { ADMIN = "Admin", EDITOR = "Editor", } const userRole = UserRole.ADMIN;

集中管理常量,避免魔数散落。Refine 核心对MutationMode的处理方式提供了另一种思路——见 data/types.ts:

export type MutationMode = "pessimistic" | "optimistic" | "undoable";

它用字符串联合类型替代枚举,不产生运行时对象、也没有反向映射开销。如果你的场景需要类型约束但不需要运行时值(例如纯配置字面量),字符串联合类型是比枚举更轻量的选择;这正是"按需选型"的体现。

混淆枚举键与值

很容易在需要键的地方用了值,或反之。数值枚举可以双向访问,但字符串枚举更严格:

enum Status { ACTIVE = "Active", INACTIVE = "Inactive", } console.log(Status.ACTIVE); // "Active" console.log(Status["Active"]); // ❌ Error: Property 'Active' does not exist

对错误不做日志记录

定时任务曾因未记录错误而静默失败。枚举同理——特别是当枚举与 API 或第三方系统交互时,应当始终校验传入的值是否正确。建议对来自外部的输入做白名单校验(如用Object.values(Enum)判断合法性),而不是直接信任。

过度复杂化数值枚举

曾有人手动给数值枚举的每个成员赋值,其实可以让 TypeScript 自动递增:

enum BillingCycle { FREE = 0, MONTHLY, // 1 YEARLY, // 2 }

避开这些错误后,代码会更可靠、更易调试。枚举用好了非常强大。

总结

本文通过为简易的分级订阅模型定义AccountTypeBillingSchedule两个枚举,系统梳理了 TypeScript 枚举的核心概念:字符串枚举必须初始化每个成员,数值枚举则不必;未初始化的首成员自动获得偏移量0,后续成员依次递增1;偏移量可设在任意位置。我们还看到字符串枚举在编译期实现单向映射、数值枚举实现双向映射,并分别了解了它们注入运行时的典型对象形态;讨论了字面量枚举表达式(常量值)与计算值的区别。最后,我们探索了枚举生成的成员级类型并据此派生自定义子类型,最终实现了一个PersonalSubscription类,完整展示了 TypeScript 枚举在对象与类型两个层面的便利性。结合仓库中 uiFrameworks.ts 与 undoableQueue/types.ts 的真实用法,以及MutationMode字符串联合类型的对照,你可以根据实际需求在枚举与联合类型之间做出合理选择。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 20:36:30

大模型高薪诚聘!小白也能入行的5大岗位及高薪秘诀,速收藏!

中国AI核心产业规模达1.2万亿&#xff0c;但人才供给不足。AI应用与集成岗位需求最大&#xff0c;薪资达4.7万/月。通用大模型与算法岗位薪资最高&#xff0c;达5.3万/月。上海反超北京成为AI人才需求占比最高的城市。教育体系加速响应&#xff0c;664所院校开设AI专业。AI行业…

作者头像 李华
网站建设 2026/9/12 20:36:16

基于SVM的恶意URL检测:特征工程与在线识别实践

简介&#xff1a;机器学习检测恶意网址改进版源码项目&#xff0c;面向计算机科学、人工智能、大数据、数学、电子信息等专业正在准备课程设计、期末大作业或毕业设计的学生&#xff0c;也适合希望接触真实算法工程的初学者参考。代码经过严格调试可直接运行&#xff0c;完整覆…

作者头像 李华
网站建设 2026/9/12 20:35:37

第 30 讲 全流程落地梳理

专栏名称:《Linux 从零基础到全场景实战:服务器・嵌入式・网络安全三合一》 文章定位:付费工程方法梳理篇;承接前序所有硬件、逻辑、固件、下载知识点,做完整工程链路梳理。从拿到官方手册开始,拆解 6 大阶段 22 个核心步骤,明确 Vivado 全流程角色定位,讲解硬件 - 逻辑…

作者头像 李华
网站建设 2026/9/12 20:34:06

嵌入式硬件契约:原理图、I2C信号完整性与启动链深度解析

1. 这不是悔过书&#xff0c;是十年嵌入式老兵的实战备忘录干了这么多年嵌入式&#xff0c;我最后悔的几件事——这句话刚在技术群里冒头&#xff0c;底下立刻刷出二十多条“1”和“泪目”。不是矫情&#xff0c;是真疼。疼在哪&#xff1f;疼在花三个月调通I2C从设备&#xff…

作者头像 李华