22 April, 20225 minute read

Typescript enums: Why you should use string values

Enumerations are found in almost every programming language there is. They're a language facility which allow developers to group together a set of named identifiers which act as constant values. You can use them almost anywhere you could use a union, but while your programming language of choice might not have support for union types it will almost certainly support enumerations.

Typescript has both enumerations and unions, and they each offer different tradeoffs in terms of developer productivity, type safety, and runtime performance. In this article I'll be focusing on enumerations

The Typescript enumeration

Enumerations are defined simply in Typescript:

Click to copy
enum Color {  Red,  Green,  Blue,}

The above code defines an enumeration Color, with identifiers Red, Green, and Blue. By default, the values of those identifiers will start at 0 and increment by one for each identifier. Concretely, this means that Color.Red === 0, Color.Green === 1, and Color.Blue === 2.

The enumeration can be used like any other type, as in this getColorName function:

Click to copy
function getColorName(color: Color) {  switch (color) {  case Color.Red: return 'red';  case Color.Green: return 'green';  case Color.Blue: return 'blue';  default: throw new Error(`Unknown color: ${color}`)  }} expect(getColorName(Color.Red)).toBe('red');

Enumerations are great because you get nice autocomplete (just type Color. in your editor of choice and pick the correct option) and they feel much nicer to use than constants. In situations where you're working with legacy code and need to customize the value of each enumeration member, you can do that like so:

Click to copy
// Static valuesenum Color {  Red = 10,  Green = 50,  Blue = 1_000,} // Dynamic valuesenum Color {  Red = 1 << 0,  Green = 1 << 1,  Blue = 1 << 2,} // String valuesenum Color {  Red = 'red',  Green = 'green',  Blue = 'blue',}

In general, you should avoid using enumerations without string values. Numeric enumerations suffer from a number of unfortunate limitations which–among other things–greatly weaken their type safety guarantees.

The downsides of numeric enumerations

Weak type safety

Believe it or not, the following code compiles:

Click to copy
enum Color {  Red,  Green,  Blue,} function getColorName(color: Color) {  // ...} getColorName(5);

Any number at all can be implicitly cast to a numeric enumeration even when that particular number is not a valid value for the enumeration which compromises the type safety of numeric enumerations. String enumerations, on the other hand, are far more strict:

Click to copy
enum Color {  Red = 'red',  Green = 'green',  Blue = 'blue',} function getColorName(color: Color) {  // ...} getColorName('foo'); // errorgetColorName('red'); // error

String enums prevent you from passing arbitrary strings which are outside the set of allowable values (such as 'foo' in the previous example), and they also prevent you from passing in valid values in the form of a string value. There's a massive win to type safety here, because you are forced to use values from the enum (e.g. getColorName(Color.Red)) instead of being allowed to pass in any number or any string that you want to.

Reverse mapping

The transpiled Javascript for the first enumeration I showed in this article looks something like the following:

Click to copy
"use strict";var Color;(function (Color) {    Color[Color["Red"] = 0] = "Red";    Color[Color["Green"] = 1] = "Green";    Color[Color["Blue"] = 2] = "Blue";})(Color || (Color = {}));

See how each line has two equal symbols? When you create a numeric enumeration, Typescript adds in a reverse mapping. This is useful in some situations where you want to determine whether a key and/or value is valid for a given enumeration:

Click to copy
'Red' in Color; // -> true0 in Color;     // -> true'foo' in Color; // -> false100 in Color;   // -> false

But it means that programmatically grabbing all of your enumeration's values via something like Object.values will also give you all of the enumeration's identifiers. String based enumerations, on the other hand, do not have reverse mappings which makes it much easier to programmatically grab its keys and values programmatically.

While reverse mappings can be useful in certain niche circumstances, in general I find it much better to have a clean separation between the identifiers of the enumeration and its values.

Poor debugging experience

Consider the following Typescript code–what do you expect will be logged to the console?

Click to copy
enum Shape {  Square,  Circle,  Triangle,} enum Color {  Red,  Green,  Blue,} interface Entity {  color: Color;  shape: Shape;} const myEntity: Entity = {  color: Color.Red,  shape: Shape.Circle,};console.log(myEntity);

What does myEntity look like once it's been logged? Because our enumeration values are just numbers, we get those numbers come through when we actually run our code–which doesn't have great glance value:

An example of what happens when you log numeric enums. The values are not immediately understandable.

In the given example it's at least somewhat obvious that the number 0 refers to a member of the Color enumeration and that the number 1 refers to a member of the Shape enumeration, but if you were to see these values logged in the wild in a real system with hundreds of different enumerations it would be very difficult to immediately recognize the meaning of these values.

String-based enumerations, on the other hand, log much nicer: you get to see the strings that you defined inside the enumeration's declaration in your logging output, which is far nicer–the difference between seeing "Circle" in your logs instead of the number 1 cannot be overstated, and you'll wind up saving time whenever you find yourself needing to debug some misbehaving bit of code.

The other option: union types

The point of this article is to convince you to use string enumerations instead of numeric enumerations, but it is worth touching on some of the obvious advantages to using union types. Enumerations are one of the few Typescript concepts which continue to exist after you run tsc as they get compiled down to a real object. The consequence of this is that each enumeration you add to your project will slightly increase your bundle size.

How much? Well, the Color enumeration we've been looking at with 3 different possible values adds about 90 bytes after transpilation and minification. A lot of that is fixed overhead–57 bytes of that minified code is dedicated to just setting up the object which stores the enumeration values, as the emitted code needs to support declaration merging.

On the other hand, union types declarations like type Color = 'red' | 'green' | 'blue'; are completely stripped out by the Typescript compiler and therefore have zero impact on bundle size. The tradeoffs you make have to do with developer experience: while the Typescript language server supports automatic refactoring of both enums and union types equally well, and you still get nice autocompletion you lose out on two things:

  1. You can't grep for uses of a union type as easily as you can an enumeration. Because the enumeration has an identifier, it's easy to search for all uses of that enumeration in your codebase.
  2. You cannot navigate to the definition of your union type from a callsite which uses the union. When you see a function call like getColorName(Color.Red) in VSCode you can ⌘+Click on the Color symbol and get taken straight to the enumeration–but you can't do that with code like getColorName('red'). Your best option in that case is to navigate to the function declaration where you can then use the parameter's type annotation, but it adds one extra step compared to code using enumerations.

The bundle size consideration is important. While 90 bytes doesn't sound like a lot, a hundred enumerations means you're spending 9 kB of your 170 kB budget on just types.

But if you are using enumerations in your Typescript code, you should be using string values. Numeric enums simply make too many tradeoffs.

Don't want to miss out on new posts?

Join 100+ fellow engineers who subscribe for software insights, technical deep-dives, and valuable advice.

Get in touch 👋

If you're working on an innovative web or AI software product, then I'd love to hear about it. If we both see value in working together, we can move forward. And if not—we both had a nice chat and have a new connection.
Send me an email at hello@sophiabits.com