在打字稿中,为什么枚举的并集不是枚举?

In typescript, why is a union of enums not also an enum?

我想合并两个枚举。

enum Color {
    RED = 'red',
    BLUE = 'blue',
}

enum Shape {
    CIRCLE = 'circle',
    SQUARE = 'square',
}

然而,当我声明联合类型并尝试引用成员时:

type ColorShape = Color | Shape;
const test: ColorShape = ColorShape.RED;

我遇到编译时错误:

TS2693: 'ColorShape' only refers to a type, but is being used as a value here.

这是为什么?枚举的联合不也是枚举本身吗?

重要的是要记住 TypeScript 类型 之间的 TypeScript 差异,这些差异存在于编译时并且 erased 来自发出的 JavaScript代码;和 JavaScript values,它们也存在于运行时并出现在发出的 JavaScript 代码中。 TypeScript 试图跟踪值和类型,但 JavaScript 只知道值。

大多数情况下,当您在 TypeScript 中进行声明时,您要么将值引入范围,要么将类型引入范围,但不会同时引入两者:

// values
const foo = 123;
function bar() { }

// types
type Baz = { a: number };
interface Qux { b: string }

上面的foobar是存在于发出的JavaScript中的值,而BazQux是被擦除的类型.

并注意值和类型具有完全不同的命名空间;您可以拥有同名的值和类型,并且它们不必相互关联。编译器不会混淆它们:

// separate namespaces
const Xyz = { z: 1 };
type Xyz = { y: string };

这里Xyz是一个值的名字(一个z属性的对象,它的值为1)也是一个类型的名字(一个y 属性 的对象类型,其值类型为 string)。这些是无关的。如果我写 const uvw = Xyz; 那么我正在写 value-level 代码,使其成为 JavaScript,因此 Xyz 是值。如果我写 interface Rst extends Xyz { } 那么我正在写 type-level 从 JavaScript 中删除的代码,因此 Xyz 是类型。


好的,所以大多数时候当你声明一些命名的东西时,那个东西要么是一个类型,要么是一个值,但不是两者。但是有两个声明确实使同名的值和类型都存在:the class declaration and the enum declaration:

// both
class Abc { c = "" }
enum Def { D = "e" }

这里的名称 Abc 指的是一个 class 构造函数 value ,你可以像 new Abc() 一样在运行时引用它,它也引用一个 class 实例 type ,您可以在编译时使用它来谈论 class 实例,如 const instance: Abc = ...。如果你写 const instance: Abc = new Abc() 你首先指的是类型,然后是值。

类似地,名称 Def 既指一个枚举对象 value,你可以像 Def.D === "e" 一样在运行时引用它,它也指一个enum type 是枚举成员值类型的联合,如 const def: Def = ...。如果你写 const def: Def = Def.D 你首先指的是类型,然后是值。


现在,虽然 class 是 JavaScript 的一部分(自 ES2015 起),但 enum 不是。因此,当您编写 enum 时,编译器实际上会将其 downlevel 变为 JavaScript。让我们看看您的 Color 枚举:

enum Color {
    RED = 'red',
    BLUE = 'blue',
}

这或多或少编译成类似于

的东西
const Color = {
  RED: "red",
  BLUE: "blue"
}

这只是一个普通的对象。同时,类型 Color 的行为类似于

type Color = "red" | "blue";

这不完全正确,因为 enum 已被“标记”,因此您不能只为它们分配一个字符串,但对于我们的目的来说已经足够接近了。所以当你的enum Color声明类似于上面的值和类型声明的组合时。


现在,当你写作时

type ColorShape = Color | Shape;

您正在创建 ColorShape 类型 。但是 type ColorShape = ... 不是 enum,所以您所做的只是创建类型。这里没有产生相应的值。您只完成了创建类似 enum 的组合的一半工作。如果你想要相应的值,你将不得不手动创建它。

你想要的是看起来像 {RED: "red", BLUE: "blue", CIRCLE: "circle", SQUARE: "square"} 的东西。有多种方法可以获取 Color 值和 Shape 值并制作这样一个对象。

你可以使用 the Object.assign() method:

const ColorShape = Object.assign({}, Color, Shape);
// const ColorShape: typeof Color & typeof Shape

或者您可以使用 object spread:

const ColorShape = { ...Color, ...Shape };
/* const ColorShape: {
    CIRCLE: Shape.CIRCLE;
    SQUARE: Shape.SQUARE;
    RED: Color.RED;
    BLUE: Color.BLUE;
} */

任何一种方法都应该有效。完成此操作后,现在,您可以(大部分)使用 ColorShape,就好像它是由 ColorShape:

const test: ColorShape = ColorShape.RED; // okay

请注意,我说的是“大部分”。 enum 声明还将更多类型引入范围;它实际上充当 namespace ,您可以在其中使用虚线路径将值枚举为类型本身:

interface RedState {
    name: string;
    color: Color.RED // <-- this is a type
}

但是您的任何一个 ColorShape 定义都不会发生这种情况:

interface RedState {
    name: string;
    color: ColorShape.RED // error!
    // 'ColorShape' only refers to a type, but is being used as a namespace here.
}

你可能不关心这个。如果你以某种方式这样做,那么我知道如何做到这一点的唯一方法是手动创建你的 ColorShape 命名空间并从中导出每种类型:

namespace ColorShape {
    export type CIRCLE = typeof ColorShape.CIRCLE;
    export type SQUARE = typeof ColorShape.SQUARE;
    export type RED = typeof ColorShape.RED;
    export type BLUE = typeof ColorShape.BLUE;
}

不太好,可能不是您需要的任何东西。但为了完整起见,我想我只是用 enums.

展示幕后发生的更多事情

Playground link to code