在TypeScript中扩展枚举的教程

1,766 阅读7分钟

TypeScript深受开发者社区的喜爱,原因有很多,其中之一是它为用其编写的代码提供了静态检查

在开发生命周期的早期发现问题,可以节省数天的调试随机的、模糊的错误,这些错误在使用JavaScript等动态语言时有时会突然出现。

TypeScript可以帮助使你的代码更具可预测性和更好的文档化,使重构更容易,并帮助减少你在生产应用中运行时可能面临的潜在错误。它的受欢迎程度和力量体现在其93%的开发者满意率和过去五年的使用量激增上。

对TypeScript至关重要的一种语言机制是枚举

什么是TypeScript中的枚举?

枚举并不是类型化的一个特征,有趣的是,就像 TypeScript 的大多数特征一样 - 事实上,它们是少数几个、增强语言的新特征之一。

枚举允许开发者为一个变量定义一套严格的选项。比如说:

enum Door {
  Open,
  Closed,
  Ajar // half open, half closed
}

Enums默认为数字枚举,所以上面的枚举本质上是一个以0,1,2 为键的对象,我们可以在转写的JavaScript代码中看到:

"use strict";
var Door;
(function (Door) {
    Door[Door["Open"] = 0] = "Open";
    Door[Door["Closed"] = 1] = "Closed";
    Door[Door["Ajar"] = 2] = "Ajar"; // half open, half closed
})(Door || (Door = {}));
console.log(Door.FullyOpened);

在TypeScript中,你也可以使用字符串枚举,像这样:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

如果你再使用这个Door 枚举,你可以确保变量只使用枚举中指定的三个选项。所以,你不可能意外地分配错误的东西,也不可能通过这种方式轻易地产生bug。

如果你真的试图使用另一个变量,它就会抛出这样一个类型错误。

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}
console.log(Door.FulyOpened)

Property 'FullyOpened' does not exist on type 'typeof Door'.

为什么我们需要扩展一个枚举?

扩展是面向对象的四大支柱之一,是TypeScript中存在的一种语言特性。扩展一个枚举允许你本质上复制一个变量定义,并向其添加一些额外的东西。

所以,举例来说,你可能想做这样的事情:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame extends Door { // This will not work!
  Missing = "noDoor"
}

console.log(DoorFrame.Missing)

我们可以在一个枚举中添加额外的属性,甚至可以将两个枚举合并在一起,以便在我们的枚举上仍然获得严格的类型,同时也可以在它们被定义之后对其进行修改。

但是请注意,上面的代码片段不起作用了!它不能转译,并抛出了一个问题。它不能转译,并且抛出了四个不同的错误。

你能扩展枚举吗?

简短的回答是不能,你不能扩展枚举,因为TypeScript没有提供语言功能来扩展它们。然而,你可以利用一些变通方法来实现继承的目的。

TypeScript中的类型交叉

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame {
  Missing = "noDoor"
}

type DoorState = Door | DoorFrame; 

在上面的代码块中,我们使用了一个交叉类型。交叉的作用就像一个 "或",这简单地意味着DoorState 类型要么是Door ,要么是DoorFrame

现在这意味着DoorState 可以互换地使用两个枚举中的任何一个变量。

扩展语法

我们在前面的转译代码中看到,一个枚举会变成一个JavaScript对象,具有你的枚举所指定的键和值。

在TypeScript中,如果我们愿意,我们可以写出纯粹的JavaScript。事实上,这是TypeScript的一个很大的优势。例如,你可以把你所有的file.js 改名为file.ts ,并关闭编译器对你的代码的检查。只要你运行编译/转译步骤,一切都会正常工作,而不需要改变代码。

我们可以利用这一点,知道当我们的枚举变成JavaScript时,它将是一个JavaScript对象字面,并使用传播语法,像下面这样

enum Move {
  LEFT = 'Left',
  RIGHT = 'Right',
  FORWARD = 'Forward',
  BACKWARD = 'Backward'
}
const myMove = {
  ...Move,
  JUMP: 'Jump'
}

不过,这个解决方案已经被描述得很次要了,因为它并不像交集类型那样是一个好的解决方案,因为它不像我们的第一个解决方案那样稳健。这是因为你的枚举的 "组成 "是在运行时发生的,而当我们使用类型交叉时,类型检查可以在编译/转写时发生,而不是运行时。

TypeScript 枚举的最佳实践

我们已经讨论了如何在Typescript中扩展枚举,但是枚举并不是用来解决所有问题的灵丹妙药。枚举如果使用不当,会使你的代码可读性、可扩展性和可维护性变差,而不是改善你的代码。

因此,让我们介绍一下在TypeScript中使用枚举时的一些最佳实践和常见模式。

1.避免异质枚举

我已经解释了我们如何能够拥有像这样的字符串枚举:

enum Seasons {
  Summer = "Summer",
  Winter = "Winter",
  Spring = "Spring",
  Fall = "Fall"
}

同时也有像这样的数字枚举:

enum Decision {
  Yes,
  No
}

但是,有第三种类型的枚举,你可能不知道,叫做异质枚举。这就是你可以在同一个枚举中使用字符串和数字枚举。

文档中的一个例子是这样的:

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

值得注意的是,即使是文档也不鼓励这种做法,因为在这个例子中,使用这种方法表明你很可能需要:

  • 重新考虑这两个变量之间的关系
  • 创建两个独立的枚举
  • 使它们都符合一个数据类型

2."枚举作为配置 "的反模式

有时,代码功能会被迫遵守一个枚举选项,这很快就会变成一种反模式。

这里有一个例子

enum Operators {
  Add,
  Subtract
}
function calculate(op: Operators, firstNumber: number, secondNumber: number) {
  switch(op) {
    case Operators.Add: return firstNumber + secondNumber
    case Operators.Subtract: return firstNumber - secondNumber
  }
} 

上面的代码看起来相当简单和安全,因为我们的例子确实是简单和安全的。

但是在大型代码库中,当你像这样严格地将实现细节与枚举类型捆绑在一起时,你会引起一些问题:

  • 你创造了两个真理的来源(如果枚举发生变化,枚举和函数都需要被更新)
  • 这种模式会在代码中散布元数据
  • 该代码块不再是通用的了

如果你需要做类似上述的事情,一个更简单(也更浓缩)的模式可以是这样的:

const Operators = {

  Add: {
    id: 0,
    apply(firstNumber: number, secondNumber: number) { return firstNumber + secondNumber }
  },

  Subtract: {
    id: 1,
    apply(firstNumber: number, secondNumber: number) { return firstNumber - secondNumber }
  }
}

你可以在这里这里阅读更多关于这种模式的信息。

3.枚举最能代表的数据类型

通常有一种方法可以将代码中利用的不同类型的数据分组:离散变量或连续变量。

离散变量是指在其表示方法之间有空格的数据,并且只有几种表示方法。这里有几个例子:

  • 一周的日子
    • 星期一
    • 星期二
    • 周三
    • 星期四
    • 星期五
    • 礼拜六
    • 阳光
  • 季节
    • 夏季
    • 冬季
    • 春季
    • 秋天

离散数据是一个很好的候选者,可以放在枚举里面,它可以帮助代码的清晰度和重复使用。连续数据指的是没有空隙的数据,它们落入一个连续的序列,比如数字。这些数据可以根据它们的测量结果而有所不同:

  • 某人的体重
  • 一辆汽车的速度

离散数据是一个很好的候选数据,它可以在枚举中使用,而连续数据不应该在枚举中使用。你能想象一个用于年龄的枚举吗?

enum Age {
  Zero,
  One,
  Two,
  Three,
  Four,
  Five,
  Six
}

这不是一个放在枚举中的好候选数据,因为它需要不断地更新和修正,导致维护的噩梦。

你应该只在枚举中添加离散的、高度稳定的数据类型。

总结

我希望这篇文章是有用的,这样你就能更好地理解什么是枚举,它们所解决的问题,合并两个枚举的用例,以及你可能实现的方法祝你黑客活动愉快。