← Back to blog Post

TypeScript enums: good or bad?

Hosted here as an English translation. Originally published on Lean Mind.

Introduction

One of the classic arguments against using TypeScript is that the generated bundle is often larger than if the same code had been written in native JavaScript. That argument carries weight, especially in frontend development, where loading time and user experience matter a lot.

In other areas, such as the backend of an application, the argument becomes less important because end users are not directly affected, and disk space on a server is rarely a major concern in modern systems.

Even so, when we return to the frontend case, there are some decisions we can make to improve bundle size. One of them is to understand what each language construct transpiles to and to look for simpler alternatives when necessary.

What is an enum in TypeScript?

An enum is essentially a way to group properties into an object-like structure with static constant values that represent business concepts such as states, categories, or numeric values.

This is especially useful for avoiding typos, removing repeated literal values spread throughout the code, and making refactors easier by centralizing those definitions.

However, this concept is not native to JavaScript; it comes from languages like Java. That means that when we use enums in TypeScript, they need to be transpiled into JavaScript, much like classes or private methods in older targets such as ES5.

In TypeScript, an enum looks like this:

export enum Fruits {
  apple = 'apple',
  orange = 'orange',
}

After transpilation, we get something closer to this:

"use strict";
var Fruits;
(function (Fruits) {
  Fruits["orange"] = "orange";
  Fruits["apple"] = "apple";
})(Fruits || (Fruits = {}));

How can we improve this?

While the difference is not dramatic in every situation, we can improve performance by using a simpler construct that stays closer to how we would solve the same problem in JavaScript.

Instead of using an enum, we can define a constant object and enforce immutability with as const:

const Fruits = {
  orange: 'orange' as const,
  apple: 'apple' as const,
}

// or better
const Fruits2 = {
  orange: 'orange',
  apple: 'apple',
} as const

In JavaScript, this becomes something as simple as:

"use strict";
const Fruits = {
  orange: 'orange',
  apple: 'apple',
};

const Fruits2 = {
  orange: 'orange',
  apple: 'apple',
};

As we can see, the output is much more direct. It is also worth remembering that features such as types and interface only exist in the TypeScript type system and are not emitted to JavaScript at all.

Now, what is the difference between the first and second constant definitions? Mostly how TypeScript interprets them. If we try to mutate the object, the compiler errors are slightly different:

Fruits.apple = 'apple2'
// Type '"apple2"' is not assignable to type '"apple"'.(2322)

Fruits2.apple = 'apple2'
// Cannot assign to 'apple' because it is a read-only property.(2540)

The second form is preferable because the error more clearly reflects the intended immutability.

Conclusion

TypeScript is a very powerful tool and, in most cases, maintainability should be prioritized over raw performance. Still, when performance and bundle size matter more than usual, we can avoid some of the heavier constructs from the superset and choose solutions that are closer to plain JavaScript without sacrificing readability.

That said, there is an important nuance. As Ulises Santana pointed out after reading the original article, the object-based alternative does not solve every case equally well. If we want the same construct to work both as a runtime value and as a type restriction, enums still provide value. With objects, the equivalent type is possible, but more verbose: typeof Fruits2[keyof typeof Fruits2].

enum-feedback

  • typescript
  • javascript
  • performance