2.2.5 • Published 28 days ago

enum-plus v2.2.5

Weekly downloads
-
License
MIT
Repository
github
Last release
28 days ago

English | 中文

npm version npm bundle size npm downloads GitHub License

⬇️    简介 | 特性 | 安装 | 枚举定义 | API | 使用方法 | 本地化 | 全局扩展    ⬇️

简介

enum-plus是一个增强版的枚举类库,完全兼容原生enum的基本用法,同时支持扩展显示文本、绑定到 UI 组件以及提供丰富的扩展方法,是原生enum的一个直接替代品。它是一个轻量级、零依赖、100% TypeScript 实现的工具,适用于多种前端框架,并支持本地化。

枚举项扩展了显示名称后,可以与枚举值用来一键生成下拉框、复选框等组件。通过枚举的扩展方法,可以轻松遍历枚举项数组,获取某个枚举值的显示文本,判断某个值是否存在等。枚举项的显示文本支持本地化,可以根据当前语言环境返回对应的文本,这样可以使得枚举项的显示文本更加灵活,更加符合用户的需求。

还有哪些令人兴奋的特性呢?请继续探索下面的技术文档吧!

特性

  • 兼容原生 enum 的用法
  • 支持numberstring等多种数据类型
  • 支持枚举项扩展显示文本
  • 支持本地化显示文本,可以使用任意国际化类库
  • 支持枚举值转换为显示文本,代码更简洁
  • 枚举项支持扩展任意个自定义字段
  • 支持将枚举绑定到 Ant DesignElementPlusMaterial-UI 或任意其它组件库,只要一行代码
  • 支持 Node.js 环境,支持服务端渲染(SSR)
  • 零依赖,纯原生 JavaScript,可以应用在任意前端框架中
  • 100% TypeScript 实现,支持类型推断
  • 轻量(gzip 压缩后仅 2KB+)

安装

使用 npm 安装:

npm install enum-plus

使用 pnpm 安装:

pnpm add enum-plus

使用 bun 安装:

bun add enum-plus

或者使用 yarn:

yarn add enum-plus

枚举定义

构造一个枚举,枚举值支持 numberstring 两种类型

  • 示例 1:基础用法,与原生枚举用法基本一致

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: 0,
  Monday: 1,
} as const);
Week.Monday; // 1
  • 示例 2:值类型使用 string

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: 'Sun',
  Monday: 'Mon',
} as const);
Week.Monday; // 'Mon'
  • 👍👍 【推荐】 示例 3(标准用法):包含 Key、Value,以及显示文本

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: { value: 0, label: '星期日' }, // 此示例不考虑本地化
  Monday: { value: 1, label: '星期一' }, // 此示例不考虑本地化
} as const);
Week.Monday; // 1
Week.label(1); // 星期一
  • 👍 示例 4:省略 value 字段

如果 value 与 Key 相同,可以考虑省略 value 字段,使用 Key 作为枚举值

import { Enum } from 'enum-plus';

const Week = Enum({
  Sunday: { label: '星期日' }, // 等价于 { value: "Sunday", label: '星期日' }
  Monday: { label: '星期一' }, // 等价于 { value: "Monday", label: '星期一' }
} as const);
Week.Monday; // 'Monday'
Week.label('Monday'); // 星期一
  • 示例 5:动态数组构建枚举

有时候我们需要使用接口返回的数据,动态创建一个枚举,这时可以采用数组的方式来初始化枚举

import { Enum } from 'enum-plus';

const petTypes = await getPetsData();
// [   { value: 1, key: 'dog', label: '狗' },
//     { value: 2, key: 'cat', label: '猫' },
//     { value: 3, key: 'rabbit', label: '兔子' }   ];
const PetTypes = Enum(petTypes);

关于更高级的用法,请参考 自定义初始化选项 章节

  • 示例 6:支持原生枚举初始化,相当于给原生枚举添加一些扩展方法

import { Enum } from 'enum-plus';

enum init {
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
}
const Week = Enum(init);
Week.Sunday; // 0
Week.Monday; // 1
Week.Saturday; // 6
Week.label('Sunday'); // Sunday

API

拾取枚举值

Enum.XXX

像原生enum一样,直接拾取一个枚举值

Week.Sunday; // 0
Week.Monday; // 1

items

{ value, label, key, raw }[]

获取一个包含全部枚举项的只读数组,可以方便地遍历枚举项。由于符合 Ant Design 组件的数据规范,因此支持将枚举一键转换成下拉框、复选框等组件,只需要一行代码,更多详情可以参考后面的例子


keys

string[]

获取一个包含全部枚举项Key的只读数组


label

方法   label(keyOrValue?: string | number): string | undefined

根据某个枚举值或枚举 Key,获取该枚举项的显示文本。如果设置了本地化,则会返回本地化后的文本。

Week.label(1); // 星期一
Week.label('Monday'); // 星期一

key

方法   key(value?: string | number): string | undefined

根据枚举值获取该枚举项的 Key,如果不存在则返回undefined

Week.key(1); // 'Monday'

has

方法   has(keyOrValue?: string | number): boolean

判断某个枚举项(值或 Key)是否存在

Week.has(1); // true
Week.has('Sunday'); // true
Week.has(9); // false
Week.has('Birthday'); // false

toSelect

方法   toSelect(config?: OptionsConfig): {value, label}[]

toSelectitems相似,都是返回一个包含全部枚举项的数组。区别是,toSelect返回的元素只包含labelvalue两个字段,同时,toSelect方法支持在数组头部插入一个默认元素,一般用于下拉框等组件的默认选项,表示全部、无值或不限等,当然你也能够自定义这个默认选项


toMenu

方法   toMenu(): { key, label }[]

生成一个对象数组,可以绑定给 Ant DesignMenuDropdown等组件

import { Menu } from 'antd';

<Menu items={Week.toMenu()} />;

数据格式为:

[
  { key: 0, label: '星期日' },
  { key: 1, label: '星期一' },
];

toFilter

方法   toFilter(): { text, value }[]

生成一个对象数组,可以直接传递给 Ant Design Table 组件的列配置,在表头中显示一个下拉筛选框,用来过滤表格数据

数据格式为:

[
  { text: '星期日', value: 0 },
  { text: '星期一', value: 1 },
];

toValueMap

方法   toValueMap(): Record<V, { text: string }>

生成一个符合 Ant Design Pro 规范的枚举集合对象,可以传递给 ProFormFieldProTable 等组件。

数据格式为:

{
  0: { text: '星期日' },
  1: { text: '星期一' },
}

raw

方法重载^1   raw(): Record<K, T[K]> 方法重载^2   raw(keyOrValue: V | K): T[K]

第一个无参数的重载,返回枚举集合的初始化对象,即用来初始化 Enum 原始 init 对象。

第二个重载方法用来处理单个枚举项,根据枚举值或枚举 Key 获取该枚举项的原始初始化对象,也就是说第二个方法是第一个方法返回值的一部分。另外,如果在枚举项上添加了额外的扩展字段的话,也可以用这种方式获取到

const Week = Enum({
  Sunday: { value: 0, label: '星期日' },
  Monday: { value: 1, label: '星期一' },
} as const);

Week.raw(); // { Sunday: { value: 0, label: '星期日' }, Monday: { value: 1, label: '星期一' } }
Week.raw(0); // { value: 0, label: '星期日' }
Week.raw('Monday'); // { value: 1, label: '星期一' }

valueType   Type-ONLY

value1 | value2 | ...

在 TypeScript 中,获取一个包含全部枚举值的联合类型,用于缩小变量或组件属性的数据类型,避免使用numberstring这种过于宽泛的类型,提高代码的可读性和类型安全性

const weekValue: typeof Week.valueType = 1;
const weeks: (typeof Week.valueType)[] = [0, 1];
type WeekValues = typeof Week.valueType; // 0 | 1

注意,这只是一个 TypeScript 类型,只能用来约束类型,不可在运行时调用,运行时调用会抛出异常


keyType   Type-ONLY

key1 | key2 | ...

valueType类似,获取一个包含全部枚举 Key 的联合类型

const weekKey: typeof Week.keyType = 'Monday';
const weekKeys: (typeof Week.keyType)[] = ['Sunday', 'Monday'];
type WeekKeys = typeof Week.keyType; // 'Sunday' | 'Monday'

注意,这只是一个 TypeScript 类型,只能用来约束类型,不可在运行时调用,运行时调用会抛出异常


rawType   Type-ONLY

{ value: V, label: string, [...] }

与无参数的raw方法类似,只不过raw方法支持在运行时调用,而rawType只能用来约束类型

注意,这只是一个 TypeScript 类型,只能用来约束类型,不可在运行时调用,运行时调用会抛出异常


使用方法

拾取枚举值,与原生枚举用法一致

const Week = Enum({
  Sunday: { value: 0, label: '星期日' },
  Monday: { value: 1, label: '星期一' },
} as const);

Week.Sunday; // 0
Week.Monday; // 1

支持添加 Jsdoc 注释,代码提示更友好

在代码编辑器中,将光标悬停在枚举项上,即可显示关于该枚举项的详细 Jsdoc 注释,而不必再转到枚举定义处查看。另外,在输入HttpCodes.时,编辑器也会自动提示枚举项列表,通过键盘上下键切换枚举项,也可以展示详细信息

const HttpCodes = Enum({
  /** Code400: 错误的请求语法。由于被认为是客户端错误(例如,错误的请求语法、无效的请求消息帧或欺骗性的请求路由),服务器无法或不会处理请求 */
  E400: { value: 400, label: 'Bad Request' },
  /** Code400: 未经授权。客户端必须对自身进行身份验证才能获得请求的响应 */
  E401: { value: 401, label: 'Unauthorized' },
  /** Code403: 禁止访问。客户端没有访问内容的权限;也就是说,它是未经授权的,因此服务器拒绝提供请求的资源。与 401 Unauthorized 不同,服务器知道客户端的身份 */
  E403: { value: 0, label: 'Forbidden' },
  /** Code404: 未找到。服务器找不到请求的资源。在浏览器中,这意味着无法识别 URL */
  E404: { value: 1, label: 'Not Found' },
} as const);

HttpCodes.E404; // 将光标悬停在 E404 上,将显示完整的Jsdoc文档注释

Http 状态码的释义内容参考自 MDN


获取包含全部枚举项的数组

Week.items; // 输出如下:
// [
//  { value: 0, label: '星期日', key: 'Sunday', raw: { value: 0, label: '星期日' } },
//  { value: 1, label: '星期一', key: 'Monday', raw: { value: 1, label: '星期一' } }
// ]

获取第一个枚举值

Week.items[0].value; // 0

检查一个值是否一个有效的枚举值

Week.has(1); // true
Week.items.some(item => item.value === 1); // true
1 instance of Week; // true

instanceof 操作符

1 instance of Week // true
"1" instance of Week // true
"Monday" instance of Week // true

支持遍历枚举项数组,但不支持修改

Week.items.length; // 2
Week.items.map((item) => item.value); // [0, 1],✅ 可遍历
Week.items.forEach((item) => {}); // ✅ 可遍历
for (let item of Week.items) {
  // ✅ 可遍历
}
Week.items.push({ value: 2, label: '星期二' }); // ❌ 不可修改
Week.items.splice(0, 1); // ❌ 不可修改
Week.items[0].label = 'foo'; // ❌ 不可修改

枚举值(或Key)转换为显示文本

Week.label(1); // 星期一,
Week.label(Week.Monday); // 星期一
Week.label('Monday'); // 星期一

枚举值转换为Key

Week.key(1); // 'Monday'
Week.key(Week.Monday); // 'Monday'
Week.key(9); // undefined, 因为不存在

添加扩展字段,数量无限制

const Week = Enum({
  Sunday: { value: 0, label: '星期日', active: true, disabled: false },
  Monday: { value: 1, label: '星期一', active: false, disabled: true },
} as const);
Week.raw(0).active // true
Week.raw(Week.Sunday).active // true
Week.raw('Sunday').active // true

转换成 UI 组件

  • items 可以直接作为组件的数据源(以 Select 组件为例)

    Ant Design | Arco Design Select

    import { Select } from 'antd';
    
    <Select options={Week.items} />;

    Material-UI Select

    import { MenuItem, Select } from '@mui/material';
    
    <Select>
      {Week.items.map((item) => (
        <MenuItem key={item.value} value={item.value}>
          {item.label}
        </MenuItem>
      ))}
    </Select>;

    Kendo UI Select

    import { DropDownList } from '@progress/kendo-react-dropdowns';
    
    <DropDownList data={Week.items} textField="label" dataItemKey="value" />;

    ElementPlus Select

    <el-select>
      <el-option v-for="item in Week.items" v-bind="item" />
    </el-select>

    Ant Design Vue | Arc Design Select

    <a-select :options="Week.items" />

    Vuetify Select

    <v-select :items="Week.items" item-title="label" />

    Angular Material Select

    HTML

    <mat-select>
      <mat-option *ngFor="let item of Week.items" [value]="item.value">{{ item.label }}</mat-option>
    </mat-select>

    NG-ZORRO Select

    HTML

    <nz-select>
      <nz-option *ngFor="let item of Week.items" [nzValue]="item.value">{{ item.label }}</nz-option>
    </nz-select>
  • toSelect方法与items类似,但允许在头部增加一个默认选项。默认选项可以是一个布尔值,也可以是一个自定义对象。

    • 如果是布尔值,则默认选项为{ value: '', label: 'All' },显示名称只支持英文。如果希望支持本地化,请在本地化方法中解析并处理enum-plus.options.all这个内置资源。关于本地化的更多详情,请参考本地化章节
    • 如果是一个对象,则可以自定义默认选项的值和显示文本,显示文本会自动支持本地化
    <Select options={Week.toSelect({ firstOption: true })} />
    // [
    //  { value: '', label: 'All' },
    //  { value: 0, label: '星期日' },
    //  { value: 1, label: '星期一' }
    // ]
    
    // 自定义头部默认选项
    <Select options={Week.toSelect({ firstOption: { value: 0, label: '不限' } })} />
  • toMenu方法可以为 Ant Design MenuDropdown 等组件生成数据源,格式为:{ key: number|string, label: string } []

import { Menu } from 'antd';

<Menu items={Week.toMenu()} />;
  • toFilter方法可以生成一个对象数组,为表格绑定列筛选功能,列头中显示一个下拉筛选框,用来过滤表格数据。对象结构遵循 Ant Design 的数据规范,格式为:{ text: string, value: number|string } []
import { Table } from 'antd';

const columns = [
  {
    title: 'week',
    dataIndex: 'week',
    filters: Week.toFilter(),
  },
];
// 在表头中显示下拉筛选项
<Table columns={columns} />;
  • toValueMap方法可以为 Ant Design ProProFormFieldsProTable等组件生成数据源,这是一个类似 Map 的数据结构,格式为:{ [key: number|string]: { text: string } }
import { ProTable } from '@ant-design/pro-components';

<ProFormSelect valueEnum={Week.toValueMap()} />;

两个枚举合并(或者扩展某个枚举)

const myWeek = Enum({
  ...Week.raw(),
  Friday: { value: 5, label: '星期五' },
  Saturday: { value: 6, label: '星期六' },
});

使用枚举值序列来缩小number类型   TypeScript ONLY

使用 valueType 类型约束,可以将数据类型从宽泛的numberstring类型缩小为有限的枚举值序列,这不但能减少错误赋值的可能性,还能提高代码的可读性

const weekValue: number = 8; // 👎 任意数字都可以赋值给周枚举,即使错误的
const weekName: string = 'Birthday'; // 👎 任意字符串都可以赋值给周枚举,即使错误的

const goodWeekValue: typeof Week.valueType = 1; // ✅ 类型正确,1 是一个有效的周枚举值
const goodWeekName: typeof Week.keyType = 'Monday'; // ✅ 类型正确,'Monday' 是一个有效的周枚举名

const badWeekValue: typeof Week.valueType = 8; // ❌ 类型错误,8 不是一个有效的周枚举值
const badWeekName: typeof Week.keyType = 'Birthday'; // ❌ 类型错误,'Birthday' 不是一个有效的周枚举名

type FooProps = {
  value?: typeof Week.valueType; // 👍 组件属性类型约束,可以防止错误赋值,还能智能提示有效值有哪些
  names?: (typeof Week.keyType)[]; // 👍 组件属性类型约束,可以防止错误赋值,还能智能提示有效值有哪些
};

自定义初始化选项

示例 5:动态数组构建枚举 章节中,介绍了可以通过后端动态数据来构建枚举,但是很可能动态数据的字段名并不是valuelabelkey,而是其它的字段名。这时你可以传入一个自定义选项,把这些映射到其它字段名上

import { Enum } from 'enum-plus';

const petTypes = await getPetsData();
// [   { id: 1, code: 'dog', name: '狗' },
//     { id: 2, code: 'cat', name: '猫' },
//     { id: 3, code: 'rabbit', name: '兔子' }   ];
const PetTypes = Enum(petTypes, {
  getValue: 'id',
  getLabel: 'name',
  getKey: 'code', // getKey可选
});
Week.items; // 输出如下:
// [   { value: 1, label: '狗', key: 'dog' },
//     { value: 2, label: '猫', key: 'cat' },
//     { value: 3, label: '兔子', key: 'rabbit' }   ]

在上面的例子中,getValuegetLabelgetKey 还可以是一个函数,用来处理更复杂的业务逻辑,比如:

const PetTypes = Enum(petTypes, {
  getValue: (item) => item.id,
  getLabel: (item) => `${item.name} (${item.code})`,
  getKey: (item) => item.code,
});

😟 命名冲突?

这里为枚举使用添加一些边界情况,从上面的用例中可以看到,我们可以通过 Week.XXX 来快捷访问枚举项,但是万一枚举项的 key 与枚举方法命名冲突怎么办?

我们知道枚举类型上还存在 labelkeytoSelect 等方法,如果与某个枚举项重名,枚举项的值优先级更高,会覆盖掉这些方法。但不用担心,你可以在 items 下访问到它们。请参考下面的代码示例:

const Week = Enum({
  foo: { value: 1 },
  bar: { value: 2 },
  keys: { value: 3 }, // 命名冲突
  label: { value: 4 }, // 命名冲突
} as const);
Week.keys; // 3,枚举项优先级更高,会覆盖掉方法
Week.label; // 4,枚举项优先级更高,会覆盖掉方法
// 可以通过 items 访问到这些方法 🙂
Week.items.keys // ['foo', 'bar', 'keys', 'label']
Week.items.label(1); // 'foo'

更极端一些,万一items与枚举项命名冲突怎么办?放心,你仍然可以通过别名字段访问到items数组。参考下面的示例:

import { ITEMS } from 'enum-plus';

const Week = Enum({
  foo: { value: 1 },
  bar: { value: 2 },
  items: { value: 3 }, // 命名冲突
} as const);

Week.items; // 3,枚举项优先级更高,会覆盖掉 items
Week[ITEMS]; // ITEMS 是一个别名Symbol
// [
//  { value: 1, key: 'foo', label: 'foo' },
//  { value: 2, key: 'bar', label: 'bar' },
//  { value: 3, key: 'items', label: 'items' }
// ]
// 等价于原来的 Week.items 🙂

本地化

enum-plus 本身不提供国际化功能,但支持通过设置 localize 可选自定义方法来实现本地化文本。你可以在项目内声明一个本地化方法,把输入的枚举label转换成对应的本地化文本。你需要自己维护语言,并且在 localize 方法中针对当前语言返回对应的文本。如果可能的话,强烈建议你使用一个流行的国际化库,比如 i18next

下面是一个简单的示例,但第一种方式并不是一个好的实践,因为它不够灵活,仅用于演示基本功能

import { Enum } from 'enum-plus';
import i18next from 'i18next';
import Localize from './Localize';

let lang = 'zh-CN';
const setLang = (l: string) => {
  lang = l;
};

// 👎 这不是一个好例子,仅为了演示基础功能,请采用后面其它的方式
const sillyLocalize = (content: string) => {
  if (lang === 'zh-CN') {
    switch (content) {
      case 'enum-plus.options.all':
        return '全部';
      case 'week.sunday':
        return '星期日';
      case 'week.monday':
        return '星期一';
      default:
        return content;
    }
  } else {
    switch (content) {
      case 'enum-plus.options.all':
        return 'All';
      case 'week.sunday':
        return 'Sunday';
      case 'week.monday':
        return 'Monday';
      default:
        return content;
    }
  }
};
// 👍 建议使用 i18next 或其他国际化库
const i18nLocalize = (content: string | undefined) => i18next.t(content);
// 👍 或者封装成一个基础组件
const componentLocalize = (content: string | undefined) => <Localize value={content} />;

const Week = Enum(
  {
    Sunday: { value: 0, label: 'week.sunday' },
    Monday: { value: 1, label: 'week.monday' },
  } as const,
  {
    localize: sillyLocalize,
    // localize: i18nLocalize, // 👍 推荐使用i18类库
    // localize: componentLocalize, // 👍 推荐使用组件形式
  }
);
setLang('zh-CN');
Week.label(1); // 星期一
setLang('en-US');
Week.label(1); // Monday

每个枚举类型都这样设置可能比较繁琐,你也可以通过 Enum.localize 方法来全局设置。如果静态设置和初始化选项两者同时存在,则枚举类型的初始化选项方式优先级更高。

Enum.localize = sillyLocalize;

全局扩展

Enum已经提供了一些常用的方法,但如果这些方法还不能满足你的需求,你可以通过 Enum.extend 方法来添加自定义扩展函数。这些扩展方法将会被添加到所有的枚举类型上,即便是在扩展之前已经创建的枚举类型,也会立即生效

App.ts

Enum.extend({
  toMySelect(this: ReturnType<typeof Enum>) {
    return this.items.map((item) => ({ value: item.value, title: item.label }));
  },
  reversedItems(this: ReturnType<typeof Enum>) {
    return this.items.reverse();
  },
});

Week.toMySelect(); // [{ value: 0, title: '星期日' }, { value: 1, title: '星期一' }]

如果你在使用 TypeScript,你可能需要再扩展一下枚举类型声明,这样可以获得更好的类型提示。在你的项目中创建或编辑一个声明文件(例如 global.d.ts),并在其中扩展全局类型。此文件可以放在项目的根目录或任意目录下,只要确保 TypeScript 能够找到它

global.d.ts

import type { EnumItemInit } from 'enum-plus';
import type { EnumItemClass } from 'enum-plus/lib/enum-item';

declare global {
  export interface EnumExtension<T, K, V> {
    toMySelect: () => { value: V; title: string }[];
    reversedItems: () => EnumItemClass<EnumItemInit<V>, K, V>[];
  }
}

请注意,你不是必须导入EnumItemInitEnumItemClass这些类型,这些仅在这个示例中被使用,为了添加更友好的类型提示。

EnumExtension接口是一个泛型接口,它接受三个类型参数,分别是:

  • T: 枚举类型的初始化对象
  • K: 枚举项的键值
  • V: 枚举项的值

如果你希望在扩展方法中提供更友好的类型提示,你或许可能需要使用到这些类型参数,但这些都是可选的,如果你不需要,可以直接省略掉它们

2.2.5

28 days ago

2.2.4

1 month ago

2.2.3

1 month ago

2.2.2

2 months ago

2.1.1-beta.1

2 months ago

2.1.0-beta.0

2 months ago

2.2.0-beta.0

2 months ago

2.2.0-beta.1

2 months ago

2.2.1

2 months ago

2.0.3

2 months ago

2.2.0

2 months ago

2.1.1

2 months ago

2.1.0

2 months ago

2.0.2

2 months ago

2.0.1

2 months ago

2.0.0-alpha.3

2 months ago

2.0.0-alpha.2

2 months ago

2.0.0-beta.1

2 months ago

2.0.0

2 months ago

1.0.3

1 year ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

1.0.0-2

2 years ago

1.0.0-1

2 years ago