Why you might be using Enums in TypeScript wrong

Duy NG
6 min readJun 22, 2024

--

The topic of using enums in TypeScript has been discussed a lot, but many developers still remain unaware of their drawbacks. Even though enums are popular and commonly used, they might not always be the best choice. In this article, I’ll share my thoughts on why enums can be a problem and show you a better way to do things.

The original version of this post is on my personal blog.

TL;DR

  • `enum` is a TypeScript-only feature and doesn’t exist in JavaScript
  • `enum` has several problems:
    — The compiled output can be hard to read and understand, leading to potential bugs.
    — Compatibility issues with type declarations, especially in projects using `isolatedModules`.
  • Use `as const` with a generic type helper: `export type TypeFrom<T> = T[keyof T]` for a simpler, more efficient alternative.

If you are a TypeScript developer, you’re likely familiar with using `enums`. However, have you ever considered what `enums` in TypeScript actually represent? It’s worth noting that JavaScript doesn’t have the `enum` feature.

It’s important to remember that TypeScript is essentially JavaScript with type, and the resulting code executed in the browser or backend environment is JavaScript.

So, how exactly do `enums` when transitioning between TypeScript and JavaScript?

Here is a simple example of an `enum` in TypeScript:

// constant.ts
export enum HttpStatusCode {
Ok = 200,
BadRequest = 400,
Authorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500,
GatewayTimeout = 503,
}

enum Color {
Red,
Green,
Blue,
Yellow = 10,
Purple,
Orange,
Pink,
}

export enum E2 {
A = 1,
B = 20,
C,
}

export enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = '123'.length,
}

And here is the JavaScript code after compilation:

// constant.js
export var HttpStatusCode;
(function (HttpStatusCode) {
HttpStatusCode[HttpStatusCode["Ok"] = 200] = "Ok";
HttpStatusCode[HttpStatusCode["BadRequest"] = 400] = "BadRequest";
HttpStatusCode[HttpStatusCode["Authorized"] = 401] = "Authorized";
HttpStatusCode[HttpStatusCode["Forbidden"] = 403] = "Forbidden";
HttpStatusCode[HttpStatusCode["NotFound"] = 404] = "NotFound";
HttpStatusCode[HttpStatusCode["InternalServerError"] = 500] = "InternalServerError";
HttpStatusCode[HttpStatusCode["GatewayTimeout"] = 503] = "GatewayTimeout";
})(HttpStatusCode || (HttpStatusCode = {}));
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
Color[Color["Yellow"] = 10] = "Yellow";
Color[Color["Purple"] = 11] = "Purple";
Color[Color["Orange"] = 12] = "Orange";
Color[Color["Pink"] = 13] = "Pink";
})(Color || (Color = {}));
export var E2;
(function (E2) {
E2[E2["A"] = 1] = "A";
E2[E2["B"] = 20] = "B";
E2[E2["C"] = 21] = "C";
})(E2 || (E2 = {}));
export var FileAccess;
(function (FileAccess) {
// constant members
FileAccess[FileAccess["None"] = 0] = "None";
FileAccess[FileAccess["Read"] = 2] = "Read";
FileAccess[FileAccess["Write"] = 4] = "Write";
FileAccess[FileAccess["ReadWrite"] = 6] = "ReadWrite";
// computed member
FileAccess[FileAccess["G"] = '123'.length] = "G";
})(FileAccess || (FileAccess = {}));

Do you see the difference between the TypeScript code and the JavaScript code? That’s quite difficult to read, right?

When compiled to JavaScript, `enums` are created as functions with the same name as our `enum` and the key-values are added to that function.

So what is the problem with enum?

1. Understanding output code

Because the code and the output do not look the same, it makes it harder to read and understand the code. This can lead to unexpected bugs at runtime.

As seen in the example above, if we specify an index base, the index base of all the subsequent keys changes. Sometimes, it can be very difficult to understand what’s going on with the output of an `enum`.

For example:

console.log(Color.Purple); // ??? 
// The output is 11.
// If we read only the TypeScript code for this case,
// we might not be sure about the result
// without understanding the underlying enum value assignments.

2. Compatibility issues with type declarations

When building projects or libraries that consume types with `.d.ts` files, using `enum` types can cause issues. Specifically, if a project uses isolatedModules, it may not be able to use `enum` types effectively. This problem has been encountered in our projects as well.

For more detailed discussions on the drawbacks to using `enum` in TypeScript, you can refer to these resources:

What is the solution instead?

The Typescript team provides a simple alternative solution for `enum`:

In modern TypeScript, you may not need an enum when an object with `as const` could suffice.

So, you might not use enum at all and use `as const` instead.

Here is an example of the output in JavaScript if we use `as const` to see how it works:

// constant.ts
// Alternative enums solutions
export const HttpStatusCodes = {
Ok: 200,
BadRequest: 400,
Authorized: 401,
Forbidden: 403,
NotFound: 404,
InternalServerError: 500,
GatewayTimeout: 503,
} as const;
export const Colors = {
Red: 0,
Green: 1,
Blue: 2,
Yellow: 3,
Purple: 11,
Orange: 5,
Pink: 6,
} as const;
// Or
export const Colors2 = {
Red: 'Red',
Green: 'Green',
Blue: 'Blue',
Yellow: 'Yellow',
Purple: 'Purple',
Orange: 'Orange',
Pink: 'Pink',
} as const;
export const FileAccesses = {
None: 0,
Read: 1 << 1,
Write: 1 << 2,
ReadWrite: 3,
G: '123'.length,
} as const;

And you will have the same code when compiled to JavaScript (without `as const`)

// constant.js
export const HttpStatusCodes = {
Ok: 200,
BadRequest: 400,
Authorized: 401,
Forbidden: 403,
NotFound: 404,
InternalServerError: 500,
GatewayTimeout: 503,
}
export const Colors = {
Red: 0,
Green: 1,
Blue: 2,
Yellow: 3,
Purple: 11,
Orange: 5,
Pink: 6,
};
export const Colors2 = {
Red: 'Red',
Green: 'Green',
Blue: 'Blue',
Yellow: 'Yellow',
Purple: 'Purple',
Orange: 'Orange',
Pink: 'Pink',
};
export const FileAccesses = {
None: 0,
Read: 1 << 1,
Write: 1 << 2,
ReadWrite: 3,
G: '123'.length,
};

The `as const` object will compile to JavaScript code that looks exactly like your TypeScript code. You can use these objects just as you would use enums. This approach resolves all the issues with enums that I mentioned above.

What about the Type for `as const`?

Using `as const` provides the constant values, but how can we define the enum type when needed? Fortunately, this is not complicated.

Here is the solution for typing:

// Helper types
export type TypeFrom<T> = T[keyof T];
// Const types
export type HttpStatusCode = TypeFrom<typeof HttpStatusCodes>;
export type Color = TypeFrom<typeof Colors>;
export type Color2 = TypeFrom<typeof Colors2>;
export type FileAccess = TypeFrom<typeof FileAccesses>;

In this approach:

  • Helper type: Define a generic helper type `TypeFrom` to extract the type from an object.
  • Const types: Use the `TypeFrom` helper to create each equivalent type for the constants.

However, there is a potential issue when importing constants and types:

import { Color } from './constant.js';
import { Color } from './definitions.js';

TypeScript does not allow the same name for different entities in the same file. A workaround is to use namespace imports:

import { Color } from './constant.js';
import * as definitions from './definitions.js';
// And use definitions.Color when needed

But I don’t recommend that approach, it’s not flexible and it makes auto-import in IDEs harder.
A better approach is to name constants and types differently, typically using plurals for `as const` objects and singular forms for types:

// constant.ts
export const HttpStatusCodes = {
Ok: 200,
BadRequest: 400,
} as const;
// definitions.ts
export type TypeFrom<T> = T[keyof T];
export type HttpStatusCode = TypeFrom<typeof HttpStatusCodes>;

This way, you can easily import and export constants and types without naming conflicts. Additionally, this approach has the advantage of clearly separating TypeScript `type` definitions from TypeScript constants/functions

I hope this article reaches many people, helps you optimize your use of TypeScript in your project.
Happy coding!

Enjoyed this article? For more technical content on Typescript or backend development, stay updated by following my blog through RSS feeds

--

--