Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.4: String Enums

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 post is part of the TypeScript Evolution series:

  1. TypeScript 2.0: Non-Nullable Types
  2. TypeScript 2.0: Control Flow Based Type Analysis
  3. TypeScript 2.0: Acquiring Type Declaration Files
  4. TypeScript 2.0: Read-Only Properties
  5. TypeScript 2.0: Tagged Union Types
  6. TypeScript 2.0: More Literal Types
  7. TypeScript 2.0: The never Type
  8. TypeScript 2.0: Built-In Type Declarations
  9. TypeScript 2.1: async/await for ES3/ES5
  10. TypeScript 2.1: External Helpers Library
  11. TypeScript 2.1: Object Rest and Spread
  12. TypeScript 2.1: keyof and Lookup Types
  13. TypeScript 2.1: Mapped Types
  14. TypeScript 2.1: Improved Inference for Literal Types
  15. TypeScript 2.1: Literal Type Widening
  16. TypeScript 2.1: Untyped Imports
  17. TypeScript 2.2: The object Type
  18. TypeScript 2.2: Dotted Properties and String Index Signatures
  19. TypeScript 2.2: Null-Checking for Expression Operands
  20. TypeScript 2.2: Mixin Classes
  21. TypeScript 2.3: Generic Parameter Defaults
  22. TypeScript 2.3: The --strict Compiler Option
  23. TypeScript 2.3: Type-Checking JavaScript Files with --checkJs
  24. TypeScript 2.3: Downlevel Iteration for ES3/ES5
  25. TypeScript 2.4: String Enums
  26. TypeScript 2.4: Weak Type Detection
  27. TypeScript 2.4: Spelling Correction
  28. TypeScript 2.4: Dynamic import() Expressions
  29. TypeScript 2.5: Optional catch Binding
  30. TypeScript 2.6: JSX Fragment Syntax
  31. TypeScript 2.7: Numeric Separators
  32. TypeScript 2.7: Strict Property Initialization
  33. TypeScript 2.8: Per-File JSX Factories
  34. TypeScript 2.8: Conditional Types
  35. TypeScript 2.8: Mapped Type Modifiers