如何更优雅地使用前端枚举?

2,036 阅读6分钟

在 TypeScript 开发中,枚举(enum)是一种常用的类型,用于表示一组命名的常量。然而,原生的 TypeScript enum 虽然简单易用,但在实际开发中却存在诸多痛点和局限性。

TypeScript 原生 enum 的痛点

1. 缺乏显示文本支持

原生 enum 只能定义简单的键值对映射,无法为枚举项添加友好的显示文本。这在需要在 UI 中展示枚举值时非常不便。

// 原生 enum 的定义方式
enum Status {
  Active = 0,
  Pending = 1,
  Rejected = 2,
}

// 在 UI 中展示时,需要额外维护一个映射关系
const statusTextMap = {
  [Status.Active]: '活跃',
  [Status.Pending]: '待处理',
  [Status.Rejected]: '已拒绝',
};

// 使用时需要手动转换
function getStatusText(status: Status): string {
  return statusTextMap[status] || '未知状态';
}

这种方式存在明显问题:

  • 显示文本与枚举定义分离,维护成本高
  • 添加新枚举项时容易忘记更新文本映射
  • 类型安全性不够,如果忘记映射某个值,TypeScript 不会提醒

2. 无法优雅地遍历枚举

原生 enum 没有提供原生方法来获取所有枚举项或遍历枚举。虽然有一些变通方法,但都不够优雅。

// 遍历数字枚举的变通方法,但字符串枚举则更复杂
function getStatusArray() {
  return Object.keys(Status)
    .filter((key) => !isNaN(Number(key)))
    .map((key) => Number(key));
}

// 这种方法既不优雅也不类型安全
const statusArray = getStatusArray(); // [0, 1, 2]

3. 与 UI 组件集成困难

原生 enum 难以直接用于 UI 组件,如下拉菜单、单选框等,通常需要额外的转换逻辑:

// 为 Select 组件生成选项,需要编写冗长的转换代码
function getStatusOptions() {
  return Object.keys(Status)
    .filter((key) => isNaN(Number(key)))
    .map((key) => ({
      value: Status[key as keyof typeof Status],
      label: statusTextMap[Status[key as keyof typeof Status]],
    }));
}

// 使用方式
<Select options={getStatusOptions()} />;

4. 缺乏国际化/本地化支持

原生 enum 没有考虑国际化需求,需要为每种语言维护单独的映射表:

// 为不同语言维护多个映射表
const statusTextMapEN = {
  [Status.Active]: 'Active',
  [Status.Pending]: 'Pending',
  [Status.Rejected]: 'Rejected',
};

const statusTextMapZH = {
  [Status.Active]: '活跃',
  [Status.Pending]: '待处理',
  [Status.Rejected]: '已拒绝',
};

// 使用时需要根据当前语言选择不同的映射表
function getLocalizedStatusText(status: Status, lang: 'en' | 'zh'): string {
  const map = lang === 'en' ? statusTextMapEN : statusTextMapZH;
  return map[status] || 'Unknown';
}

5. 无法扩展自定义属性

如果需要为枚举项添加额外属性(如图标、颜色、权限等),原生 enum 无法满足:

// 需要为每个属性维护单独的映射
enum Status {
  Active = 0,
  Pending = 1,
  Rejected = 2,
}

const statusColorMap = {
  [Status.Active]: 'green',
  [Status.Pending]: 'orange',
  [Status.Rejected]: 'red',
};

const statusIconMap = {
  [Status.Active]: 'check-circle',
  [Status.Pending]: 'clock-circle',
  [Status.Rejected]: 'close-circle',
};

enum-plus:一个全面增强的枚举解决方案!

面对原生 enum 的这些局限性,enum-plus 应运而生。它在保持与原生 enum 完全兼容的同时,提供了一系列增强功能。

核心特性:解决原生 enum 的痛点

1. 内置显示文本支持

import { Enum } from 'enum-plus';

const Status = Enum({
  Active: { value: 0, label: '活跃' },
  Pending: { value: 1, label: '待处理' },
  Rejected: { value: 2, label: '已拒绝' },
} as const);

// 直接获取显示文本
Status.label(0); // '活跃'
Status.label(Status.Active); // '活跃'

2. 便捷的枚举遍历

// 获取所有枚举项
Status.items; // [{ value: 0, label: '活跃' }, { value: 1, label: '待处理' }, { value: 2, label: '已拒绝' }]

// 获取所有枚举键
Status.keys; // ['Active', 'Pending', 'Rejected']

// 检查值是否存在于枚举中
Status.has(0); // true
Status.has('Active'); // true

3. 与 UI 组件无缝集成

// React + Ant Design
import { Select } from 'antd';

// 一行代码集成,无需额外转换
<Select options={Status.items} />

// 或者添加全部选项
<Select options={Status.toSelect({ firstOption: true })} />

// 适配不同组件库的格式
<Table columns={[{ filters: Status.toFilter() }]} />
<Menu items={Status.toMenu()} />

4. 本地化/国际化支持

import i18next from 'i18next';

// 设置本地化函数,与任何i18n库集成
Enum.localize = (key?: string) => i18next.t(key);

const Status = Enum({
  Active: { value: 0, label: 'status.active' },
  Pending: { value: 1, label: 'status.pending' },
  Rejected: { value: 2, label: 'status.rejected' },
} as const);

// 自动返回当前语言的翻译文本
Status.label(0); // 根据当前语言返回翻译后的文本

5. 自定义字段扩展

const Status = Enum({
  Active: {
    value: 0,
    label: '活跃',
    color: 'green',
    icon: 'check-circle',
    permission: 'user',
  },
  Pending: {
    value: 1,
    label: '待处理',
    color: 'orange',
    icon: 'clock-circle',
    permission: 'admin',
  },
  Rejected: {
    value: 2,
    label: '已拒绝',
    color: 'red',
    icon: 'close-circle',
    permission: 'admin',
  },
} as const);

// 访问自定义字段
Status.raw(0).color; // 'green'
Status.raw(Status.Active).icon; // 'check-circle'

enum-plus 的其它优势

除了解决原生 enum 的局限性外,enum-plus 还提供了许多其他优势:

1. 更好的类型安全

enum-plus 提供了精确的类型推断,可以缩小变量的取值范围,防止无效赋值:

// 使用 valueType 缩小变量类型范围
type StatusType = typeof Status.valueType; // 0 | 1 | 2

// 无效值会在编译时报错
const status: typeof Status.valueType = 5; // 类型错误!

2. 支持 JSDoc 注释与智能提示

const Status = Enum({
  /** 账户处于活跃状态 */
  Active: { value: 0, label: '活跃' },
  /** 账户等待审核 */
  Pending: { value: 1, label: '待处理' },
  /** 账户已被拒绝 */
  Rejected: { value: 2, label: '已拒绝' },
} as const);

// 光标悬停在 Status.Pending 上会显示注释和值提示

3. 动态创建枚举

支持从 API 数据中动态创建枚举,非常适合后端驱动的配置:

// 从API获取数据
const statusData = await fetchStatusTypes();
// [{ id: 1, name: 'active', displayName: '活跃' }, ...]

// 映射字段创建枚举
const Status = Enum(statusData, {
  getValue: 'id',
  getLabel: 'displayName',
  getKey: 'name',
});

4. 全局扩展机制

可以通过全局扩展机制添加自定义方法:

// 扩展自定义方法
Enum.extends({
  getActiveItems(this: ReturnType<typeof Enum>) {
    return this.items.filter((item) => item.raw.isActive);
  },
  toDropdown(this: ReturnType<typeof Enum>) {
    return this.items.map((item) => ({
      key: item.value,
      label: item.label,
      icon: item.raw.icon,
    }));
  },
});

// 所有枚举实例都可以使用这些方法
Status.getActiveItems();
Status.toDropdown();

5. 跨框架兼容性

完全支持各种前端框架(React、Vue、Angular)和流行的 UI 库(Ant Design、Element Plus、Material-UI 等)。

6. 零依赖与轻量级

enum-plus 是一个零依赖的库,gzip 压缩后仅 2KB+,不会增加项目的体积负担。

总结

TypeScript 原生 enum 虽然简单,但在实际开发中存在诸多局限性,尤其是在构建复杂企业应用时。enum-plus 作为一个轻量级增强库,在保持完全兼容原生语法的同时,解决了这些痛点,并提供了更多实用功能。

无论是枚举与 UI 集成、国际化支持、还是自定义扩展,enum-plus 都提供了简洁而强大的解决方案。通过减少样板代码和提高类型安全,它可以显著提升开发效率和代码质量。

如果你正在使用 TypeScript 开发前端应用,尤其是那些有复杂枚举需求的项目,强烈推荐尝试 enum-plus,体验它带来的便利。

源码与文档

了解更多或开始使用 enum-plus,请访问其 GitHub 仓库:github.com/shijistar/e…


希望这篇文章能帮助你了解 enum-plus 及其优势。

如果你喜欢这个项目,欢迎在 GitHub 上给项目点个 Star (⭐),可以让更多开发者发现它!

如果有任何问题或建议,欢迎在 GitHub 上提出 issue 或贡献代码。