Antd 最佳实践:枚举值如何使用

本文价值:将学会前端渲染枚举的几种常见做法以及 as constsatisfies 的妙用。

打破应当废弃 TS 枚举的说法。

比如某个字段表示含义为

ts 复制代码
/**
 * 1 - 进行中
 * 2 - 排队中
 * 3 - 已完成
 * 4 - 已停止
 * 5 - 失败
 */

我们需要将其渲染在表格中同时需要渲染在详情中。

有两种写法

1. 类型优先

首先在 service 层定义其类型 ITrainStatus

ts 复制代码
// foo.service.ts
export enum ITrainStatus {
  Progressing = 1,
  Queueing = 2,
  Completed = 3,
  Stopped = 4,
  Failed = 5,
}

// 使用
export const FooService = {
  prefix: '/api/v1/foo',
  
  async list(params): Promise<Data<IListRespData>> {
    return request(`${this.prefix}/list`, { params })
  },
}

type IListRespData = {
  id: number;
  trainStatus: ITrainStatus;
  ...
}

虽然大部分情况 union 应该优先于枚举,但是此处的状态并非字符串,不具备可读性,故应该使用枚举或常量当做枚举。我们先用枚举写一遍。

如果我们使用 ProTable 可以利用其 valueEnum:

tsx 复制代码
// utils.tsx
export const TrainStatusValueEnum: Record<
  ITrainStatus | 'all',
  { text: string; status: BadgeProps['status'] }
> = {
  all: { text: '全部', status: 'default' },
  1: {
    text: '进行中',
    status: 'processing',
  },
  2: {
    text: '排队中',
    status: 'warning',
  },
  3: {
    text: '已完成',
    status: 'success',
  },
  4: {
    text: '已停止',
    status: 'default',
  },
  5: {
    text: '失败',
    status: 'error',
  },
};

export function mapStatusToBadge(status: ITrainStatus) {
  const config = TrainStatusValueEnum[status];
  return config ? (
    <Badge status={config.status} text={config.text} />
  ) : (
    <Badge status="default" text={`未知 (${status})`} />
  );
}

注意:此处类型标注 ITrainStatus | 'all' 故以后新增类型,若忘记修改此处 TS 将报错。

第一个常量给列表页用,第二个给详情页用。

列表页渲染:

tsx 复制代码
// list.tsx
[
    {
      valueType: 'select',
      valueEnum: TrainStatusValueEnum,
    },
]

详情页渲染:

tsx 复制代码
状态:{mapStatusToBadge(status)}

2. 常量当做枚举

先定义常量

ts 复制代码
// service.ts
export const TrainStatus = {
  Progressing: 1,
  Queueing: 2,
  Completed: 3,
  Stopped: 4,
  Failed: 5,
} as const;

然后从常量推导枚举用作类型标注(也仅做类型标注):

ts 复制代码
// service.ts
/**
 * 1 - 进行中
 * 2 - 排队中
 * 3 - 已完成
 * 4 - 已停止
 * 5 - 失败
 */
type ITrainStatus = typeof TrainStatus[keyof typeof TrainStatus];

有 3 个注意点:

  1. 我们导出的是常量,类型仅用在标注。
  2. 其他地方应该用导出的常量。比如:

Good:

ts 复制代码
const stopped = status === TrainStatus.Stopped;

Bad(魔法数字):

ts 复制代码
const stopped = status === 4;
  1. 常量定义增加 as const 否则 ITrainStatus 类型将为 number 而非 1 | 2 | 3 | 4 | 5 当然如果不介意可读性也没关系。

小结:和枚举没有本质区别反倒多写了代码,为了不用枚举而不用不可取。

3. UI 配置优先

接下来我们用配置写一遍。这种方式 UI 为先,其他代码可自动推导生成。

ts 复制代码
const TrainStatusConfig = {
  1: {
    text: '进行中',
    status: 'processing',
  },
  2: {
    text: '排队中',
    status: 'warning',
  },
  3: {
    text: '已完成',
    status: 'success',
  },
  4: {
    text: '已停止',
    status: 'default',
  },
  5: {
    text: '失败',
    status: 'error',
  },
} satisfies Record<number, { text: string; status: BadgeProps['status'] }>;

hover 效果:

最后加 satisfies 是为了能自动提示 status 的值。如果标注类型也能为何用 satisfies?标注类型的话 TrainStatusConfig 的 key 类型将为 number,最后推导 ITrainStatus 也将是 number 而非数字枚举,可读性将下降。

不推荐的写法(类型标注):

Bad:

ts 复制代码
const TrainStatusConfig: Record<number, { text: string; status: BadgeProps['status'] }> = {
  1: {
    text: '进行中',
    status: 'processing',
  },
  2: {
    text: '排队中',
    status: 'warning',
  },
  3: {
    text: '已完成',
    status: 'success',
  },
  4: {
    text: '已停止',
    status: 'default',
  },
  5: {
    text: '失败',
    status: 'error',
  },
};

hover 效果:

看不到内容,可读性太差

还可以这么写(增加 as const,可读性进一步增强,连 text 内容都能显示,也更安全修改将报错):

ts 复制代码
} as const satisfies Record<number, { text: string; status: BadgeProps['status'] }>;

hover 效果:

其他都可以通过其生成:

ts 复制代码
const TrainStatusValueEnum = {
  ...TrainStatusConfig,
  all: { text: '全部', status: 'default' },
}
ts 复制代码
type ITrainStatus = keyof typeof TrainStatusConfig

当然我们还得自动生成一个常量 map 用来消除魔法数字,否则只能和数字作比较可读性下降了 const stopped = status === 4

ts 复制代码
const TrainStatus = Object.entries(TrainStatusConfig).reduce((acc, [key, { text }]) => {
  acc[text] = Number(key) as ITrainStatus;
  return acc;
}, {} as Record<IText, ITrainStatus>)
// const TrainStatus: Record<"进行中" | "排队中" | "已完成" | "已停止" | "失败", 5 | 1 | 2 | 3 | 4>

type IText = (typeof TrainStatusValueEnum)[ITrainStatus]['text'];
// type IText = "进行中" | "排队中" | "已完成" | "已停止" | "失败"

使用:

ts 复制代码
const stopped = status === TrainStatus.已停止;

mapStatusToBadge 不变省略

优点:

  • 新增状态修改一处即可,其他代码自动生成

缺点:

  • service 依赖 UI 貌似不太合适
  • 只能使用中文做 key 且 TrainStatus.已停止 hover 将显示数字 union 而非精确的 4

总结:

如果服务端返回枚举无论是数字还是字符串都应该使用枚举。通过常量虽然新增状态只需修改一处,但是会降低可读性代码复杂度也会增加。

相关推荐
小林ixn2 小时前
深入 React useState:从闭包陷阱到性能优化的完整指南
react.js·性能优化
触底反弹4 小时前
🚀 从 DOM0 级到 React 合成事件:前端事件监听的 20 年演进史
前端·react.js·面试
GitLqr8 小时前
Vue 3.6 Vapor Mode vs React 19 Compiler:前端架构的两条分叉路
vue.js·react.js·vapor
我是大卫11 小时前
【图】React源码解析-从数据结构、调度器、源码设计模式到状态计算引擎,深挖useReducer原理
前端·react.js·源码
kisshyshy11 小时前
给端侧大模型装上“发动机”:React 合成事件 + 进度条组件全解
前端·react.js·node.js
空中湖15 小时前
Spring AI Agent 编排:ReAct 模式 + 多 Agent 协作实战
人工智能·spring·react.js
小林ixn1 天前
从 ??= 到 onKeyDown:一个 React 组件的“自我修养”
前端·javascript·react.js
半个落月1 天前
用 React 搭一个 WebGPU 模型加载页:从状态驱动到可复用进度条
前端·react.js
无人生还1 天前
第 1 篇:从 Vue3 到 React 的思维转变
前端·vue.js·react.js