尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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的取值是Personal、Startup、Enterprise、Custom之一billingSchedule的取值是Free、Monthly、Quarterly、Yearly之一。这些可选项表达了按账户类型与计费周期对订阅进行分组的意图非常适合用 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); // 0AccountType.PERSONAL与BillingSchedule.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 字符串枚举当枚举的所有成员都是字符串值时它就是字符串枚举如AccountTypeenum 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, }后续成员会依次自动加1console.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); // 6TypeScript 枚举在编译期与运行时的表现编译时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.PERSONAL和AccountType.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 TAccountAccountType { tier: AccountType; postsQuota: number; verified: boolean; }; interface IBillingBillingSchedule { tier: BillingSchedule; startDate: string | boolean; expiryDate: string | boolean; } class PersonalAccount implements TAccountAccountType.PERSONAL { tier: AccountType.PERSONAL AccountType.PERSONAL; postsQuota 2; verified false; } class FreeBilling implements IBillingBillingSchedule.FREE { tier: BillingSchedule.FREE BillingSchedule.FREE; startDate false; expiryDate false; } interface IPersonalSubscriptionTAccount, IBilling { accountType: TAccount; billingSchedule: IBilling; creditCard: string; } class PersonalSubscription implements IPersonalSubscription TAccountAccountType.PERSONAL, IBillingBillingSchedule.FREE { accountType new PersonalAccount(); billingSchedule new FreeBilling(); creditCard: string XXXXXXXXXXXXXXXX; }上面的代码中BillingSchedule使用了全部未初始化的数值枚举第一个成员被赋为0后续成员依次递增1。我们用泛型把AccountType和BillingSchedule类型分别传入TAccount与IBilling使它们在PersonalAccount、FreeBilling类以及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 则用ActionTypesADD、REMOVE、DECREASE_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.tsexport 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 }避开这些错误后代码会更可靠、更易调试。枚举用好了非常强大。总结本文通过为简易的分级订阅模型定义AccountType与BillingSchedule两个枚举系统梳理了 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),仅供参考
返回列表