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!