Why you might be using Enums in TypeScript wrong
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.
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.
- Performance impacts due to runtime creation and initialization.
- 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?
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.
Compatibility issues with type declarations
When building projects or libraries that consume types with
.d.ts
files, usingenum
types can cause issues. Specifically, if a project uses isolatedModules, it may not be able to useenum
types effectively. This problem has been encountered in our projects as well.Performance impactTheenum
object is compiled to a function in JavaScript, which needs to be created and initialised at runtime each time theenum
is called. This can affect performance, especially in large applications.When compiled to JavaScript, enums use an Immediately Invoked Function Expression (IIFE). This IIFE runs once to initialise an object representing the enum. After initialisation, accessing enum values is efficient as it involves simple property lookups, with no additional function calls. Therefore, there is not a significant performance difference between using enums and const objects.
Thanks to David Dios for the correction
For more detailed discussions on the drawbacks to using enum
in TypeScript, you can refer to these resources:
- The official TypeScript documentation
- TypeScript Enums are TERRIBLE. Here's Why
- TypeScript Enums: The Good, The Bad, and The Ugly
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. Feel free to read the comments or provide feedback on my medium post. Happy coding!
Enjoyed this article? For more technical content on Typescript or backend development, stay updated by subcribing to my blog via RSS feed.