Marius Schulz
Marius Schulz
Front End Engineer

String Enums in TypeScript

TypeScript 2.4 implemented one of the most requested features: string enums, or, to be more precise, enums with string-valued members.

It is now possible to assign a string value to an enum member:

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

The string enum can be used like any other enum in TypeScript:

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

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

Here's the ES3/ES5 output that the compiler generates for the above code:

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) {
  // ...
});

This output almost looks like the output that the compiler would generate for enums with numeric members, except that there's no reverse mapping for string-valued members.

#No Reverse Mapping for String-Valued Enum Members

TypeScript emits some mapping code for each enum which constructs a mapping object. For string-valued enum members, this mapping object defines mappings from key to value, but not vice versa:

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

This means we can resolve a value by its key, but we cannot resolve a key by its value:

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

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

Compare this to an enum with number-valued members:

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

In this case, the compiler additionally emits a reverse mapping from value to key:

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

This reverse mapping allows use to resolve both a key by its value and a value by its key:

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

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

#Inlining Enum Members with a const enum

To avoid paying the cost of the generated enum mapping code, we can turn our MediaTypes enum into a const enum by adding the const modifier to the declaration:

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

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

With the const modifier in place, the compiler will not emit any mapping code for our MediaTypes enum. Instead, it will inline the value for each enum member at all use sites, potentially saving a few bytes and the overhead of the property access indirection:

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

But what if, for some reason, we need access to the mapping object at runtime?

#Emitting a const Enum with preserveConstEnums

Sometimes, it might be necessary to emit the mapping code for a const enum, for instance when some piece of JavaScript code needs access to it. In this case, you can turn on the preserveConstEnums compiler option in your tsconfig.json file:

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

If we compile our code again with the preserveConstEnums option set, the compiler will still inline the MediaTypes.JSON usage, but it will also emit the mapping code:

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) {
  // ...
});

This article and 44 others are part of the TypeScript Evolution series. Have a look!