本文价值:将学会前端渲染枚举的几种常见做法以及
as const
和satisfies
的妙用。打破应当废弃 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 个注意点:
- 我们导出的是常量,类型仅用在标注。
- 其他地方应该用导出的常量。比如:
Good:
ts
const stopped = status === TrainStatus.Stopped;
Bad(魔法数字):
ts
const stopped = status === 4;
- 常量定义增加
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
总结:
如果服务端返回枚举无论是数字还是字符串都应该使用枚举。通过常量虽然新增状态只需修改一处,但是会降低可读性代码复杂度也会增加。