前端"枚举"管理指南

0 阅读4分钟

写在前面

郑重承诺以下内容不由 AI 生成

前端里的“枚举”,本质上是在解决以下问题:

  • 限制状态范围
  • 提高可读性
  • 提高类型安全
  • 避免魔法字符串
  • 方便 UI 映射
  • 方便 AI 理解

下面作者为同学们总结了前端领域常见的枚举方式:

Typescript 的 enum 枚举类型

enumTypescript 原生支持的枚举类型。许多同学不知道的是,枚举的定义方式是有区别的,定义方式会决定 enum 的应用方式,分为 enumconst enum

enum

支持定义数字和字符串,定义/使用方式如下:

enum Status {
  Pending,
  Success,
  Failed
}

Status.Pending // 0
enum Status {
  Pending = 'pending',
  Success = 'success',
  Failed = 'failed'
}

Status.Pending // pending

数字类型支持反向映射,如:

Status[Status.Pending] // Pending

字符串类型不支持反向映射,因为 enum 编译后会输出大致这样的代码:

// input
enum Status {
  Pending,
  Success,
  Failed
}

// output
const Status = {
  Pending: 0,
  Success: 1,
  Failed: 2,

  0: "Pending",
  1: "Success",
  2: "Failed"
}

字符串会在设计层面有很多限制,这里随便举个例子:

// input
enum Vector {
  X = 'Y',
  Y = 'Z',
  Z = 'X'
}

// output 命名空间已经明显乱掉了, 假设访问 Vector['X'],都不知道访问的是哪一个。
const Vector = {
  X: 'Y',
  Y: 'Z',
  Z: 'X',

  Y: 'X',
  Z: 'Y',
  X: 'Z'
}

const enum 枚举

上文的 enum 是包含 运行时 的,Typescript 会编译出一个 Javascript 对象,但对 bundle 尺寸和性能有要求的库,需要一种更轻的枚举类型,这就是后来 Typescript 团队支持的 const enum 特性。定义/使用方式如下:

const enum Status {
  Pending,
  Success,
  Failed
}

Status.Pending // 0

它不支持双向映射,因为 ts 会在编译时将 Status 抹去,只把 Status.Pending 的调用直接替换成字面量 0。

对象枚举

绝大部分的前端工程师的工作还是以业务为主,更多只是单纯的寻求 好用 + 实用 + 类型安全,基本不会在意 bundle 尺寸,实际上对于 Web App 来说,枚举也不会带来明显的 bundle 尺寸增长。 这也催生出了对象枚举的定义方式,优势在于开发者可以自由的定义映射,类型使用也更加明确简单。

const Status = {
  Pending: 'pending',
  Success: 'success',
  Failed: 'failed'
} as const 

// as const 是必须的,影响下面的 type Status,对 as const 感兴趣的同学可以去问一下 AI,这里就不展开了。

const StatusLabels = {
  [Status.Pending]: '准备中',
  [Status.Success]: '成功',
  [Status.Failed]: '失败',
}

// 这个类型工具可以提取成通用工具
type ValueOf<T> = T[keyof T]

type Status = ValueOf<typeof Status> // 'pending' | 'success' | 'failed'

Status.Pending // 'pending'
StatusLabels[Status.Pending] // '准备中'

enum-plus

对象枚举虽然一定程度的解决了业务问题,但定义起来实在有点复杂,虽然这对于 AI 来说不是什么难事,但枚举的信息密度非常低,不够内聚,关注点不够聚焦,AI 也经常犯迷糊。 但好在开发社区也有解决方案,比如 enum-plus

import { Enum } from 'enum-plus'

const WeekEnum = Enum({
  Sunday: { value: 0, label: 'I love Sunday' },
  Monday: { value: 1, label: 'I hate Monday' },
});

WeekEnum.Sunday; // 0
WeekEnum.items[0].key; // 'Sunday'
WeekEnum.items[0].label; // 'I love Sunday'

作者找到这个库的时候也是想要直接集成到项目里的,因为它功能非常丰富,提供了 非常非常多 的 api。但同时我也觉得 api 过于复杂了,并且似乎类型约束相对松散,无法直接完成迁移(除非牺牲类型安全)比如:

import { Enum } from 'enum-plus'

const WeekEnum = Enum({
  Sunday: { value: 0, label: 'I love Sunday' },
  Monday: { value: 1, label: 'I hate Monday' },
})

console.log(WeekEnum.Sunday)
// 这里不会报类型错误,但 `2` 不是一个合法的枚举值,enum-plus 在这种情况下会返回 undefined,虽然也是合理的,但作者更倾向于严格一些类型约定。
console.log(WeekEnum.label(2))

enumOf

最后是作者自己的版本,个人比较满意。实现较为轻量,api 也比较简单,并且类型安全,国际化支持也非常容易,同时开源并集成到了 rattail 工具库里,有需要的同学可以自取。下面是常用案例。

import { enumOf } from 'rattail'

const Status = enumOf({
  Success: { value: 0, label: 'Success' },
  // 字段支持函数返回,国际化支持很简单。
  Warning: { value: 1, label: () => t('global.warning') },
})

Status.Success // 0
Status.label(Status.Success) // 'Success'
Status.options()
/*
[
  { value: 0, label: 'Success'},
  { value: 1, label: 'Warning' },
]
*/
Status.values() // [0, 1]
Status.labels() // ['Success', 'Warning']

你也可以借助 rattail 内置的函数去调整 .options() 返回的数据结构,这个在业务开发里很常见。

import { rekey } from 'rattail'

Status.options().map(option => rekey(option, { value: 'key' }))
/*
[
  { key: 0, label: 'Success'},
  { key: 1, label: 'Warning' },
]
*/

你也可以选择扩展更多字段,它们都会有完善的类型推导。

const Status = enumOf({
  Success: { value: 1, label: 'Success', color: 'green' },
  Warning: { value: 2, label: 'Warning', color: 'orange' },
})

Status.option(Status.Success).color // 'green'

写在最后

感觉您耐心看完这篇文章,希望您能喜欢。这里是《前端毕业班》,前端开发者的自救互助小组。在 AI 与不确定性并存的时代,我们一起看清焦虑,聊技术、聊趋势,也聊前端还能走多远,走去哪。