Typescript 有联合,那么枚举是多余的吗?

Typescript has unions, so are enums redundant?

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

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

和一个类似的联合类型声明:

type X: "A" | "B" | "C"
var x: X = "A";

如果它们基本上服务于相同的目的,并且联合更强大、表现力更强,那么为什么需要枚举?

据我所知,它们并不是多余的,原因很简单,联合类型纯粹是一个编译时概念,而枚举实际上被转译并最终得到结果 javascript (sample).

这允许你用枚举做一些事情,否则联合类型是不可能的(比如 enumerating the possible enum values

您可能想要使用 enum

的几个原因

我看到使用联合的最大优点是它们提供了一种简洁的方式来表示具有多种类型的值,而且它们非常可读。 let x: number | string

编辑:从 TypeScript 2.4 开始,枚举现在支持字符串。

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

枚举在概念上可以看作是联合类型的一个子集,专用于int and/or 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 enums: How do they work? What can they be used for?


联合类型支持异构数据和结构,支持多态性,例如:

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# 以了解良好的建模实践。来自 article on F# enums on F# for fun and profit 的引用在这里可能很有用:

In general, you should prefer discriminated union types over enums, unless you really need to have an int (or a string) value associated with them

模型枚举还有其他替代方法。其中一些在另一篇 2ality 文章中有很好的描述 Alternatives to enums in TypeScript

enum 类型并不冗余,但大多数情况下 union 是首选。

但并非总是如此。使用枚举来表示例如状态转换可能比使用 union**

更方便和更有表现力

考虑真实场景:

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

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

With the recent versions of TypeScript, it is easy to declare iterable union types. Therefore, you should prefer union types to enums.

How to declare iterable union types

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 the actual values of the union type do not describe theirselves very well, you can name them as you do with enums.

// 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
}

Do not miss as const assertion which plays the crucial role in these patterns.

Why it is not good to use enums?

1。 Non-const enums do not fit to the concept "a typed superset of JavaScript"

I think this concept is one of the crucial reasons why TypeScript has become so popular among other altJS languages. Non-const enums violate the concept by emitting JavaScript objects that live in runtime with a syntax that is not compatible with JavaScript.

2。 Const enums have some pitfalls

Const enums cannot be transpiled with Babel

There are currently two workarounds for this issue: to get rid of const enums manually or with plugin babel-plugin-const-enum.

Declaring const enums in an ambient context can be problematic

Ambient const enums are not allowed when the --isolatedModules flag is provided. A TypeScript team member says that "const enum on DT really does not make sense" (DT refers to DefinitelyTyped) and "You should use a union type of literals (string or number) instead" of const enums in ambient context.

Const enums under --isolatedModules flag behave strangely even outside an ambient context

I was surprised to read this comment on GitHub and confirmed that the behavior is still true with TypeScript 3.8.2.

3. Numeric enums are not type safe

You can assign any number to numeric enums.

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

4. Declaration of string enums can be redundant

We sometimes see this kind of string enums:

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

I have to admit that there is an enum feature that cannot be achieved by union types

Even if it is obvious from the context that the string value is included in the enum, you cannot assign it to the enum.

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

This unifies the style of enum value assignment throughout the code by eliminating the use of string values or string literals. This behavior is not consistent with how TypeScript type system behaves in the other places and is kind of surprising and some people who thought this should be fixed raised issues (this and this), in which it is repeatedly mentioned that the intent of string enums is to provide "opaque" string types: i.e. they can be changed without modifying consumers.

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;

Note that this "opaqueness" is not perfect as the assignment of enum values to string literal types is not limited.

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;

If you think this "opaque" feature is so valuable that you can accept all the drawbacks I described above in exchange for it, you cannot abandon string enums.

How to eliminate enums from your codebase

With the no-restricted-syntax rule of ESLint, as described.