0%

typescript-use-const-instead-of-enum

介绍

在昨天的文章中,我们介绍了如何使用typescript中的枚举类型Enum来处理状态值。今天,我们将探讨一个更好的实践:使用const代替枚举。为什么说const是更好的方案呢,因为typeScript中的枚举类型有一些弊端。

枚举类型的弊端

枚举类型无法实现真正的类型安全

对于数字类型的枚举,无法实现真正的类型安全,比如下面的枚举类型定义:

1
2
3
4
5
6
7
enum JobStatus {
PENDING = 1,
RUNNING = 2
SUCCESS = 3,
FAILED = 4,
CANCELED = 5,
}

这里面定义了五种类型的状态,分别用数字1到5来表示。但是这里有一个隐藏的问题,你可以将任意数字赋值给JobStatus类型的变量,而不管这个数字是否在1到5之间。例如:

1
2
let status: JobStatus = 1; // 这是合法的
status = 6; // 这也是合法的,虽然6不是枚举定义的

这就导致了类型安全性的问题,枚举类型并不能限制状态值只能是1到5之间的数字。

一个更好的做法是使用字符串类型代替数字类型:

1
2
3
4
5
6
7
const enum JobStatus {
PENDING = 'PENDING',
RUNNING = 'RUNNING',
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
CANCELED = 'CANCELED',
}

这样就可以确保状态值只能是枚举定义的字符串,而不能是其他任意字符串。

1
2
const status: JobStatus = JobStatus.PENDING; // 这是合法的
const status: JobStatus = 'INVALID'; // 这将导致编译错误

枚举类型不支持Tree-Shaking

Tree-Shaking是指在打包时去除未使用的代码。TypeScript的枚举类型在编译后会生成一个对象,这个对象包含了所有枚举成员的映射关系。即使程序中没有使用这个枚举类型,编译后的代码中仍然会保留这个对象。这会导致打包后的代码体积增大。

使用const代替枚举

上面提到的两点问题,使用const可以很好地解决。下面使用const来实现同样的功能,首先定义Job状态常量,因为要兼顾后端接口的整数类型和前端显示的字符串类型,我们索性将其封装到一起。用code表示状态值,用label表示状态的显示名称。

1
2
3
4
5
6
7
8
9
const JobStatus = {
PENDING: {code: 1, label: 'PENDING'},
RUNNING: {code: 2, label: 'RUNNING'},
SUCCESS: {code: 3, label: 'SUCCESS'},
FAILED: {code: 4, label: 'FAILED'},
CANCELED: {code: 5, label: 'CANCELED'},
} as const;

type JobStatusType = typeof JobStatus[keyof typeof JobStatus];

接下来定义Job接口,使用JobStatusType来描述状态类型。

1
2
3
4
5
interface Job {
id: string;
name: string;
status: JobStatusType;
}

后端返回的数据和先前一样用数字类型来表示Job状态。

1
2
3
4
5
6
7
const jobs = [
{ id: '1', name: 'Job 1', status: 1 },
{ id: '2', name: 'Job 2', status: 2 },
{ id: '3', name: 'Job 3', status: 3 },
{ id: '4', name: 'Job 4', status: 4 },
{ id: '5', name: 'Job 5', status: 5 },
];

最后是打印Job的函数,这里涉及到一个问题,我们不能像之前一样直接使用字符串来访问状态的显示名称,因为现在状态是一个对象,我们需要通过状态的code来获取对应的label

1
2
3
4
function printJobStatus(job: Job) {
const status = Object.values(JobStatus).find(s => s.code === job.status);
console.log(`Job ${job.id} (${job.name}) is currently ${status?.label ?? 'UNKNOWN'}.`);
}

现在我们可以调用这个函数来打印Job的状态:

1
jobs.forEach(printJobStatus);

这样做的好处是可以省去之前的映射函数getJobDisplayName,因为我们在定义JobStatus时已经将状态码(code)和显示名称(label)封装在一起了。

1
2
3
4
5
6
7
8
// 该函数可以省略
const getJobDisplayName: Record<JobStatus, string> = {
[JobStatus.PENDING]: 'Pending',
[JobStatus.RUNNING]: 'In progress',
[JobStatus.SUCCESS]: 'Success',
[JobStatus.FAILED]: 'Failed',
[JobStatus.CANCELED]: 'Canceled',
};

弊端是多了一个查找的过程,但这个查找过程是非常轻量级的,因为我们只需要遍历一次JobStatus对象来找到对应的状态。

const代替枚举的好处在于:

  1. 类型安全:使用const可以确保状态值只能是预定义的状态,而不能是其他任意值。
  2. Tree-Shakingconst支持Tree-Shaking,只有实际使用才会被保留,从而减小打包后的代码体积。

好了,今天就到这里了,我们明天见。