Typescript 中的字符串枚举

2021-02-21

Typescript 2.4 实现了一个呼声很高的特性:字符串枚举,或者更准确地说,成员的值是 string 类型的枚举。

现在允许将一个字符串赋值给一个枚举成员:

enum MediaTypes {
  JSON = "application/json",
  XML = "application/xml",
}

字符串枚举可以像其他 Typescript 中的枚举一样被使用:

enum MediaTypes {
  JSON = "application/json",
  XML = "application/xml",
}

fetch("https://example.com/api/endpoint", {
  headers: {
    Accept: MediaTypes.JSON,
  },
}).then(response => {
  // ...
});

下面是编译器生成的 ES3/ES5 代码:

var MediaTypes;
(function (MediaTypes) {
  MediaTypes["JSON"] = "application/json";
  MediaTypes["XML"] = "application/xml";
})(MediaTypes || (MediaTypes = {}));
fetch("https://example.com/api/endpoint", {
  headers: {
    Accept: MediaTypes.JSON,
  },
}).then(function (response) {
  // ...
});

输出的结果和数字成员类型的枚举编译输出的几乎一样,但字符串成员的枚举没有反向的映射。

字符串枚举成员没有反向映射

对于每一个枚举,Typescript 都会生成一些映射代码构造出一个映射对象。但对于字符串枚举成员,这个映射对象只定义了 key 到 value 的映射,没有反向的映射。

var MediaTypes;
(function (MediaTypes) {
  MediaTypes["JSON"] = "application/json";
  MediaTypes["XML"] = "application/xml";
})(MediaTypes || (MediaTypes = {}));

这意味着,我们可以通过 key 来得到 value,但不能通过 value 得到它的key:

MediaTypes["JSON"]; // "application/json"
MediaTypes["application/json"]; // undefined

MediaTypes["XML"]; // "application/xml"
MediaTypes["application/xml"]; // undefined

让我们来对比下数字类型成员的枚举:

enum DefaultPorts {
  HTTP = 80,
  HTTPS = 443,
}

在这个例子中,编译器额外生成了一个 value 到 key 的反向映射:

var DefaultPorts;
(function (DefaultPorts) {
  DefaultPorts[(DefaultPorts["HTTP"] = 80)] = "HTTP";
  DefaultPorts[(DefaultPorts["HTTPS"] = 443)] = "HTTPS";
})(DefaultPorts || (DefaultPorts = {}));

这个反向映射允许我们既可以通过 value 获得 key,也可以通过 key 获得 value。

DefaultPorts["HTTP"]; // 80
DefaultPorts[80]; // "HTTP"

DefaultPorts["HTTPS"]; // 443
DefaultPorts[443]; // "HTTPS"

通过常量枚举(const enum)来内联枚举的成员

为了避免生成枚举映射代码带来的开销,我们可以将 MediaTypes 枚举转换成一个常量枚举,只需要在声明的时候加上 const 修饰符:

const enum MediaTypes {
  JSON = "application/json",
  XML = "application/xml",
}

fetch("https://example.com/api/endpoint", {
  headers: {
    Accept: MediaTypes.JSON,
  },
}).then(response => {
  // ...
});

加了 const 之后,编译器不会再为我们的 MediaTypes 生成任何的映射代码。相反的,它会在所有使用枚举成员的地方内联具体的值,这样减少了一些代码以及避免属性访问的开销。

fetch("https://example.com/api/endpoint", {
  headers: {
    Accept: "application/json" /* JSON */,
  },
}).then(function (response) {
  // ...
});

但是,出于某些原因,我们想在运行时访问这个映射对象怎么办?

通过 preserveConstEnums 生成常量枚举代码

有些时候,为一个常量枚举生成映射代码是必要的,比如有一些 Javascript 代码需要访问它的时候。对于这种场景,你可以在 tsconfig.json 文件中开启 preserveConstEnums 编译选项。

{
  "compilerOptions": {
    "target": "es5",
    "preserveConstEnums": true
  }
}

在设置 preserveConstEnums 之后,再次编译我们的代码,编译器依然会內联 MediaTypes.JSON 的值,但是它同时会生成映射代码:

var MediaTypes;
(function (MediaTypes) {
  MediaTypes["JSON"] = "application/json";
  MediaTypes["XML"] = "application/xml";
})(MediaTypes || (MediaTypes = {}));
fetch("https://example.com/api/endpoint", {
  headers: {
    Accept: "application/json" /* JSON */,
  },
}).then(function (response) {
  // ...
});
郑超的独立博客