8.4 Const Enums: Inlined Values and Their Limitations
Alright, let’s talk about const enums. You’re going to love the concept and hate the implementation. It’s one of those classic TypeScript features that’s brilliant in theory but comes with a giant asterisk the size of Jupiter.
Here’s the elevator pitch: a const enum is completely erased from your compiled JavaScript. Unlike a regular enum, which generates a lookup object in the final code, a const enum has its members inlined directly wherever you use them. It’s the ultimate form of type-safe abstraction with zero runtime cost. Sounds perfect, right? Well, hold my drink.
How They Work (The Good Part)
You define one just like a regular enum, but with the const modifier.
const enum LogLevel {
Debug,
Info,
Warn,
Error
}
function logMessage(level: LogLevel, message: string) {
// ... implementation
}
// This call...
logMessage(LogLevel.Error, "Everything is on fire");
// ...gets compiled to this JavaScript:
logMessage(3 /* Error */, "Everything is on fire");
See what happened? The LogLevel.Error reference was replaced by its numeric value, 3. The LogLevel object itself doesn’t exist at runtime. This is incredibly efficient. No unnecessary object properties, no generated IIFEs—just pure, raw values. It’s like getting all the benefits of a named constant for cleaner, more maintainable code, but paying none of the runtime performance tax. This is why you’d want to use them.
The Giant, Glaring Limitation
Remember how I said they’re completely erased? This isn’t just a feature; it’s a fundamental characteristic that leads to their biggest limitation: you can’t use them in situations where you need the enum to exist as a runtime value.
The most common and frustrating pitfall is trying to iterate over the members or access them dynamically. You simply can’t.
const enum Status {
Success = "SUCCESS",
Failure = "FAILURE"
}
// This works fine at compile time...
console.log(Status.Success); // Inlines to: console.log("SUCCESS" /* Success */);
// These are COMPILE ERRORS because 'Status' doesn't exist at runtime:
console.log(Object.keys(Status)); // Error: 'Status' only exists as a type.
let key: keyof typeof Status; // Error: The same issue. 'typeof' on a value that won't exist.
This makes const enums useless for things like populating a dropdown menu from the enum values or validating a string input against the enum. If your code needs to reflect over the enum, a const enum is a non-starter.
The Tooling and Ecosystem Problem
Here’s where it gets truly absurd. The inlining behavior is performed by the TypeScript compiler during compilation. This means that if you’re writing a library and you export a const enum for your users, their TypeScript compiler must be able to see the original source code of your enum to perform the inlining. If they consume your compiled library as a .d.ts file and JavaScript, the information needed for inlining is gone.
This creates a nasty compatibility issue. Tools like Babel, or other transpilers that don’t have full access to the TypeCompiler’s type information, often can’t handle const enums at all. They’ll just see SomeEnum.Value and have no idea what to replace it with, leading to runtime errors.
To mitigate this, the TypeScript compiler has a flag called --preserveConstEnums. This flag is a hilarious admission of defeat. It tells the compiler, “Okay, do the inlining like a good const enum, but also please generate the runtime object you were supposed to eliminate, just in case something else needs it.” It completely negates the primary performance benefit of using a const enum in the first place.
So, When Should You Actually Use Them?
Use const enums only when you can check all these boxes:
- You control the entire application codebase. No publishing them in libraries.
- You are certain you will never need to access the enum members dynamically. No
Object.values(MyEnum), nofor...inloops. - You are solely using the TypeScript compiler (tsc) for your build process. No Babel-only setups.
In practice, this makes them a good fit for internal application constants—things like magic numbers, keycodes, or status flags that are used extensively throughout your code and where the performance of avoiding a runtime lookup actually matters (which, let’s be honest, in 95% of apps it doesn’t).
For almost everything else, just use a regular enum or a union type of string literals (type Status = "SUCCESS" | "FAILURE";). The union type is often the better choice as it inlines just like a const enum but doesn’t pretend to be a runtime object, making its limitations much more obvious and less surprising. The const enum is a powerful tool, but it’s one you need to reach for with extreme caution and a full understanding of the trade-offs. It’s not a default choice; it’s a very specific optimization.