TypeScript有联合类型,那么枚举是否已经过时了?

199

从TypeScript引入联合类型以来,我想知道是否有任何理由声明枚举类型。考虑以下枚举类型的声明:

enum X { A, B, C }
var x: X = X.A;

还有一个类似的联合类型声明:

type X: "A" | "B" | "C"
var x: X = "A";
如果它们基本上具有相同的作用,而联合更加强大且表达力更强,那么为什么需要枚举?

2
枚举映射到数字,我猜在某些情况下可能会有所帮助。我假设他们也想最终对枚举进行更多操作,例如赋予它们可调用的属性(就像C#或Java中的枚举)。 - PaulBGD
5个回答

232

最近的TypeScript版本中,声明可迭代联合类型变得容易。因此,您应该优先使用联合类型而不是枚举。

如何声明可迭代联合类型

const permissions = ['read', 'write', 'execute'] as const;
type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute'

// you can iterate over permissions
for (const permission of permissions) {
  // do something
}

当联合类型的实际值不能很好地描述它们自己时,您可以像枚举一样对它们进行命名。

// when you use enum
enum Permission {
  Read = 'r',
  Write = 'w',
  Execute = 'x'
}

// union type equivalent
const Permission = {
  Read: 'r',
  Write: 'w',
  Execute: 'x'
} as const;
type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x'

// of course it's quite easy to iterate over
for (const permission of Object.values(Permission)) {
  // do something
}

不要错过在这些模式中起到关键作用的as const断言。

为什么不使用枚举?

1. 非常量枚举不符合“JavaScript的类型超集”概念

我认为这个概念是TypeScript在其他altJS语言中变得如此流行的关键原因之一。 非常量枚举通过发出在运行时存活的JavaScript对象来违反该概念,其语法与JavaScript不兼容。

2. 常量枚举存在一些问题

无法使用Babel转译常量枚举

目前有两种解决方法:手动或使用插件babel-plugin-const-enum来摆脱常量枚举。

在环境上下文中声明常量枚举可能会出现问题

当提供了--isolatedModules标志时,不允许声明环境常量枚举。 TypeScript团队成员表示,在DT上使用"const enum真的没有意义"(DT指DefinitelyTyped),并且应该在环境上下文中使用字面量(字符串或数字)的联合类型,而不是常量枚举。

--isolatedModules标志下的常量枚举即使不在环境上下文中也会表现奇怪

我很惊讶地看到GitHub上这条评论,并确认该行为仍然适用于TypeScript 3.8.2。

3. 数字枚举不是类型安全的

您可以将任何数字分配给数字枚举。

enum ZeroOrOne {
  Zero = 0,
  One = 1
}
const zeroOrOne: ZeroOrOne = 2; // no error!!

4. 字符串枚举的声明可能会冗余

我们有时会看到这种类型的字符串枚举:

enum Day {
  Sunday = 'Sunday',
  Monday = 'Monday',
  Tuesday = 'Tuesday',
  Wednesday = 'Wednesday',
  Thursday = 'Thursday',
  Friday = 'Friday',
  Saturday = 'Saturday'
}

我不得不承认,有一种枚举特性是联合类型无法实现的

即使从上下文中很明显字符串值包含在枚举中,也不能将其赋值给枚举。

enum StringEnum {
  Foo = 'foo'
}
const foo1: StringEnum = StringEnum.Foo; // no error
const foo2: StringEnum = 'foo'; // error!!

通过消除使用字符串值或字符串字面量,这统一了整个代码中枚举值分配的样式。这种行为与 TypeScript 类型系统在其他地方的行为不一致,有些人认为应该修复这种情况,并提出了问题(thisthis),其中反复提到字符串枚举的意图是提供“不透明”的字符串类型:即它们可以在不修改使用者的情况下更改。

enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// As this style is forced, you can change the value of
// Weekend.Saturday to 'Sat' without modifying consumers
const weekend: Weekend = Weekend.Saturday;

请注意,这种“不透明性”并不完美,因为将枚举值分配给字符串文字类型并没有限制。
enum Weekend {
  Saturday = 'Saturday',
  Sunday = 'Sunday'
}
// The change of the value of Weekend.Saturday to 'Sat'
// results in a compilation error
const saturday: 'Saturday' = Weekend.Saturday;

如果您认为这个“opaque”特性非常有价值,可以接受我上面描述的所有缺点作为交换条件,那么您不能放弃字符串枚举。
如何从代码库中消除枚举
使用ESLint的no-restricted-syntax规则,如所述

1
我认为在环境上下文中声明的内容无法进行迭代。提供声明 declare const permissions: readonly ['read', 'write', 'execute'] 和真实的 JavaScript 对象 const permissions = ['read', 'write', 'execute'] 应该可以解决问题。 - kimamula
5
我不希望将字符串字面值赋值或与枚举进行比较。const foo: StringEnum === StringEnum.Foo // true or false 我希望这个限制可以确保我们不会混淆字符串字面值。 - Matt
1
联合类型模式更酷。@kimamula 这是一个不太有力的论点。这来自于这个非常好的回答中提出的模式的支持者。 - maninak
2
使用枚举,可以更轻松地在项目中查找值的所有用法,以进行重命名、添加、删除新值等操作。对我来说,当我在 switch 语句中忘记一个 case 时,我的 IDE 给出警告非常方便。 - transang
1
@maninak 感谢您的评论。我同意并已将其删除。 - kimamula
显示剩余5条评论

102
就我所见,它们并不是冗余的,因为联合类型仅仅是一个编译时概念,而枚举实际上被转译后出现在结果JavaScript中(样例)。

这使得您可以对枚举执行一些联合类型无法实现的操作,例如枚举可能的值)。


2
我认为你的意思是有使用枚举类型的原因。第一句话有些令人困惑。“就我所看到的,枚举类型不是…”回答了“是否有声明枚举类型的原因”的问题。 - Matt
3
在我的阅读中,第一句话的“据我所见,它们不是”指的是标题问题中的枚举类型:“...那么枚举类型是不是多余的?”。我将提交一个编辑来表达这个意思。 - manroe
我认为枚举类型“集中”了值的定义,而与字符串字面量的联合则不然……如果您更改字符串字面量的值,则必须在使用它的每个地方都进行更改;如果您更改枚举类型的值,则会自动在所有地方进行更改。我喜欢不重复的事物,因为重复通常显示出一种模式,而这些模式可以封装在适当的抽象中。 - Victor
正如@kimamula的回答所示,使用联合类型创建可迭代的类型非常容易,这样可以实现其他情况下无法实现的功能,比如枚举可能的枚举值。 - undefined

38

有几个原因你可能想要使用枚举

我认为使用联合类型的重要优势在于它们提供了一种简洁的方式来表示具有多种类型的值,并且它们非常易读。 let x: number | string

编辑:自TypeScript 2.4起,枚举现在支持字符串。

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
} 

8
我是不是唯一一个认为在“位标志”中使用enum是一种反模式的人? - Cameron Tacklind
2
取决于你在做什么。在JavaScript中,位操作并不像以前那样罕见了。 - snarf

20

枚举可以在概念上被视为联合类型的子集,专门用于 int 和/或 string 值,并具有其他一些在其他回答中提到的附加功能,使其易于使用,例如命名空间

关于类型安全性,数字枚举最不安全,然后是联合类型,最后是字符串枚举:

// Numeric enum
enum Colors { Red, Green, Blue }
const c: Colors = 100; // ⚠️ No errors!

// Equivalent union types
type Color =
    | 0 | 'Red'
    | 1 | 'Green'
    | 2 | 'Blue';

let color: Color = 'Red'; // ✔️ No error because namespace free
color = 100; // ✔️ Error: Type '100' is not assignable to type 'Color'

type AltColor = 'Red' | 'Yellow' | 'Blue';

let altColor: AltColor = 'Red';
color = altColor; // ⚠️ No error because `altColor` type is here narrowed to `"Red"`

// String enum
enum NamedColors {
  Red   = 'Red',
  Green = 'Green',
  Blue  = 'Blue',
}

let namedColor: NamedColors = 'Red'; // ✔️ Error: Type '"Red"' is not assignable to type 'Colors'.

enum AltNamedColors {
  Red    = 'Red',
  Yellow = 'Yellow',
  Blue   = 'Blue',
}
namedColor = AltNamedColors.Red; // ✔️ Error: Type 'AltNamedColors.Red' is not assignable to type 'Colors'.

在这篇2ality文章中可以了解更多关于这个主题的内容:TypeScript枚举:它们是如何工作的?可以用来做什么?


联合类型支持不同类型和结构的数据,使得多态成为可能:

class RGB {
    constructor(
        readonly r: number,
        readonly g: number,
        readonly b: number) { }

    toHSL() {
        return new HSL(0, 0, 0); // Fake formula
    }
}

class HSL {
    constructor(
        readonly h: number,
        readonly s: number,
        readonly l: number) { }

    lighten() {
        return new HSL(this.h, this.s, this.l + 10);
    }
}

function lightenColor(c: RGB | HSL) {
    return (c instanceof RGB ? c.toHSL() : c).lighten();
}

在枚举类型和联合类型之间,单例类型可以替代枚举类型。这样更冗长但也更加面向对象:

class Color {
    static readonly Red   = new Color(1, 'Red',   '#FF0000');
    static readonly Green = new Color(2, 'Green', '#00FF00');
    static readonly Blue  = new Color(3, 'Blue',  '#0000FF');

    static readonly All: readonly Color[] = [
        Color.Red,
        Color.Green,
        Color.Blue,
    ];

    private constructor(
        readonly id: number,
        readonly label: string,
        readonly hex: string) { }
}

const c = Color.Red;

const colorIds = Color.All.map(x => x.id);

我倾向于查看F#以了解良好的建模实践。以下是来自F# for fun and profit关于F#枚举的文章中的一句话,这里可能很有用:

通常情况下,你应该首选判别联合类型而不是枚举,除非你确实需要将它们与int(或string)值相关联

还有其他的替代方案来对枚举进行建模。其中一些在这篇Typescript中替代枚举的文章中有很好地描述。


3

枚举类型并非多余,但在大多数情况下,联合类型更受青睐。

但并非总是如此。使用枚举类型来表示状态转换等内容比使用联合类型更方便和表达力更强。

考虑实际场景:

enum OperationStatus {
  NEW = 1,
  PROCESSING = 2,
  COMPLETED = 4
}

OperationStatus.PROCESSING > OperationStatus.NEW // true
OperationStatus.PROCESSING > OperationStatus.COMPLETED // false

4
我认为如果你这样做,应该在枚举声明中明确地分配数值,以防止重新排序(例如因为其他人认为按字母顺序排序更有意义)可能会引入一个被忽视的运行时错误。 - dpoetzsch
1
如果我需要像上面那样比较操作的状态,我会称之为代码异味。 - Ali Motevallian
我不认为这是代码异味。如果服务器只是提供数据,它不需要知道你如何处理前端的数据。这样一来,前端就有责任决定如何使用数据,并在需要时在不需要服务器工作的情况下进行更改。此外,在不想为状态分配一个单词的情况下,这是非常有用的。数字非常适合描述状态。另一个例子可能是账户权限。与其存储“admin”,你可以只存储0。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接