Senior 5 min · March 05, 2026

TypeScript Enums — Reverse Mapping Blows Up Bundle to 12KB

12KB bundle bloat from enum reverse mapping and decorator metadata undefined — tree-shake safe alternatives: const enums and as const fixes.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Enums define a set of named constants that compile to either numeric reverse-mapped objects or plain constants
  • Regular enums generate a runtime object with reverse mapping, doubling bundle size for consumer code
  • const enums are completely inlined at compile time — zero runtime overhead, but break when used with isolatedModules
  • Decorators are functions applied with @ syntax that run at class definition time; order matters
  • The reflect-metadata polyfill is required for decorator metadata — missing it causes silent failures
  • Biggest mistake: assuming const enums can be consumed across packages without a companion .d.ts
Plain-English First

Imagine a traffic light. It can only ever be red, yellow, or green — not 'purple' or 42. An enum is exactly that: a named set of allowed values so your code can never accidentally use an invalid one. A decorator is like a sticky note you put on a function or class that says 'hey, before you run this, also do THIS' — the way a hotel doorman checks your key card before letting you into a room, without you having to write that check inside every room.

TypeScript enums and decorators sit at opposite ends of the language's complexity spectrum — one is deceptively simple, the other is genuinely advanced — but production codebases constantly misuse both. Enums end up bloating your bundle because developers don't understand how they compile. Decorators get cargo-culted from Angular or NestJS without anyone understanding the metadata system underneath. Both mistakes are expensive and embarrassing in a code review.

Enums exist because magic strings and magic numbers are silent killers. When your REST API returns status code 3 and your switch statement handles 1, 2, and 4, nothing explodes — the wrong branch just quietly runs forever. Decorators exist because cross-cutting concerns — logging, validation, authorization, caching — don't belong inside your business logic, and the alternative (manually wrapping every method) doesn't scale. Both features are TypeScript's answer to 'how do we write large, team-maintained codebases without losing our minds?'

By the end of this article you'll understand exactly what JavaScript TypeScript emits for each enum variant, when a const enum will save your bundle and when it will burn you, how decorators hook into ES2022's reflect-metadata system, how to write a production-grade method decorator from scratch, and the three mistakes that trip up even experienced engineers. You'll also have answers ready for the interview questions that separate senior candidates from mid-level ones.

What is TypeScript Enums and Decorators?

TypeScript Enums and Decorators is a core concept in JavaScript. Rather than starting with a dry definition, let's see it in action and understand why it exists.

An enum is a data type that restricts a variable to a set of predefined named constants. It makes your code self-documenting and prevents invalid assignments. Decorators are a way to annotate or modify classes, methods, properties, or parameters at design time. They originated from the experimental feature in JavaScript and are heavily used in frameworks like Angular and NestJS.

```typescript // Enum example enum LogLevel { INFO = 'info', WARN = 'warn', ERROR = 'error' }

// Decorator example function uppercase(target: any, propertyKey: string) { let value = target[propertyKey]; const getter = () => value.toUpperCase(); const setter = (v: string) => { value = v; }; Object.defineProperty(target, propertyKey, { get: getter, set: setter }); }

class Greeter { @uppercase name = 'john'; } ```

The enum ensures LogLevel.INFO is always a valid string. The decorator intercepts property access to transform the value. Both are compile-time patterns that influence runtime behaviour.

ForgeExample.javaJAVASCRIPT
1
2
3
4
5
6
7
8
// TheCodeForge — TypeScript Enums and Decorators example
// Always use meaningful names, not x or n
public class ForgeExample {
    public static void main(String[] args) {
        String topic = "TypeScript Enums and Decorators";
        System.out.println("Learning: " + topic + " 🔥");
    }
}
Output
Learning: TypeScript Enums and Decorators 🔥
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick.
Production Insight
Using regular enums in a shared library forces consumers to ship a reverse-mapping object they can't tree-shake away.
The emitted code is a self-invoking function that assigns both numeric and string keys — a side effect that bundlers treat as unsafe.
Rule: Library exports should prefer const enum or plain union types; regular enums belong only in private app code.
Key Takeaway
Enums prevent magic strings; decorators remove cross-cutting duplication.
Both features emit non-obvious JavaScript — always check the compiled output.
Understand the runtime cost before you add a feature.
When to Use Enum vs Union Type vs const enum
IfYou need runtime iteration over all values (e.g., for a dropdown)
UseUse a regular enum — it generates a runtime object you can iterate.
IfYou control the consumer and build pipeline, and want zero runtime overhead
UseUse a const enum — it inlines values at compile time.
IfThe enum is part of a public npm library
UseAvoid regular enum. Use const enum with declaration: true and export the const enum, or use a plain object with as const.
IfThe enum values are just strings and never used in comparisons with numbers
UseUse a union of string literals — no runtime object, better autocomplete, no reverse mapping.

How TypeScript Enums Compile to JavaScript

Understanding the compiled output of enums is critical for making the right choice between regular, const, and string enums. When you write enum Status { Active, Inactive }, TypeScript outputs something like:

``javascript var Status; (function (Status) { Status[Status["Active"] = 0] = "Active"; Status[Status["Inactive"] = 1] = "Inactive"; })(Status || (Status = {})); ``

This is an IIFE that builds an object with both forward (Status.Active → 0) and reverse (Status[0] → "Active") mappings. The reverse mapping is a feature that allows you to get the string name from a numeric value. However, it also means the entire enum object is a side effect — bundlers like Webpack or Rollup treat this as unsafe for tree-shaking, so even if you only use Status.Active, the whole object stays in the bundle.

Const enums are different. They're completely removed at compile time and replaced with literal values. const enum Status { Active = 0 } becomes just 0 wherever Status.Active is used. This gives zero runtime overhead but breaks when the enum is exported across module boundaries and isolatedModules is enabled. In that case, TypeScript warns with TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.

String enums (enum Status { Active = 'ACTIVE' }) compile to an IIFE as well, but without reverse mapping because string values can't be reversed uniquely (the compiler doesn't generate Status["ACTIVE"] = "ACTIVE"). They're safer for library exports but still carry the IIFE overhead.

enums-compile-examples.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Regular enum — has reverse mapping
enum HttpStatus { OK = 200, NotFound = 404 }

// Compiled output includes IIFE
// var HttpStatus;
// (function (HttpStatus) {
//   HttpStatus[HttpStatus["OK"] = 200] = "OK";
//   HttpStatus[HttpStatus["NotFound"] = 404] = "NotFound";
// })(HttpStatus || (HttpStatus = {}));

// Const enum — inlined at usage
const enum Direction { Up = 'UP', Down = 'DOWN' }
let move = Direction.Up; // compiles to 'UP'

// String enum — no reverse mapping
enum Color { Red = 'RED', Green = 'GREEN' }
// Only forward mapping emitted
Mental Model: Enum as a Two-Way Map
  • Forward mapping: Status.Active0
  • Reverse mapping: Status[0]"Active"
  • Const enums lose the reverse mapping but gain zero-cost lookup.
  • String enums only have forward mapping — no reverse.
  • The IIFE wrapper is a side effect that prevents tree-shaking, even if only one key is used.
Production Insight
A shared library with 3 regular enums added 8KB to the final bundle — every consumer paid that tax.
Switching to const enums with preserveConstEnums: true removed the IIFE but kept the .d.ts for consumers.
Rule: Never export a regular enum from an npm package; prefer const enum or union types.
Key Takeaway
Regular enums generate an IIFE with reverse mapping; const enums inline at zero cost.
The IIFE is a side effect that bundlers can't tree-shake.
Use const enums for internal use and union types or as const objects for public APIs.

Const Enums vs Regular Enums: Production Trade-offs

The choice between regular and const enums is a trade-off between developer experience and bundle size. Here's the decision framework:

Regular enums** are good when
  • You need reverse mapping (e.g., converting a numeric API response to a readable label).
  • You're in a codebase that doesn't use a bundler or uses tsc alone.
  • You want the enum to be iterable (e.g., for generating UI options).
Const enums** are good when
  • You control the build pipeline and can disable isolatedModules.
  • The enum values are never used in reflection or comparison with other enums.
  • You're writing a library that you want consumers to tree-shake aggressively.

But beware: const enums have a hidden cost. When exported from a library and consumed in a project with isolatedModules: true, they become ambient — meaning you can't access them at runtime. The fix is to add preserveConstEnums: true in your library's tsconfig, which will emit a regular enum alongside the const enum so consumers can use it. But then you lose the bundle savings.

An alternative that many senior engineers use: skip enums entirely and use as const objects with union types. This gives you compile-time safety, no runtime overhead, and great IDE support — at the cost of slightly more verbose syntax.

Production Insight
A team migrated an Angular app from regular enums to as const objects and saved 34KB in production bundle.
The refactor took 2 hours but removed all IIFE overhead and made tree-shaking effective.
Rule: For new code, prefer as const over enums — you get the same type safety with less runtime cost.
Key Takeaway
Const enums eliminate runtime overhead but break across modules with isolatedModules.
Regular enums provide reverse mapping and module safety at the cost of bundle size.
The modern preference is as const — same safety, zero IIFE, full tree-shakability.

Decorator Syntax and Execution Order

Decorators in TypeScript are functions that receive the decorated element and can modify it. There are four types: class decorators, method decorators, accessor decorators, and property decorators. Parameter decorators exist but are rarely used.

Execution order is fixed: property decorators run first (top to bottom), then accessor, then method, then parameter, and finally class decorator. Within the same type, decorators run from bottom to top (i.e., the one closest to the method declaration runs first). This matters when you combine multiple decorators.

A decorator factory is a function that returns the actual decorator. It allows you to pass parameters: @log('info'). The factory runs once when the class is defined, and the returned decorator runs during decoration.

``typescript function log(level: string) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log([${level}] Calling ${propertyKey} with, args); return original.apply(this, args); }; return descriptor; }; } ``

decorator-order.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function first() {
  console.log('factory: first');
  return function (target: any, key: string, desc: PropertyDescriptor) {
    console.log('decorator: first');
  };
}

function second() {
  console.log('factory: second');
  return function (target: any, key: string, desc: PropertyDescriptor) {
    console.log('decorator: second');
  };
}

class Example {
  @first @second
  method() {}
}
// Output:
// factory: first
// factory: second
// decorator: second    (bottom-most decorator runs last in factory order? Actually: bottom decorator runs first for same target)
// decorator: first

// For property decorators, order is top-to-bottom by definition.
Production Insight
A team had a @authenticate and @validate on the same method. The validation decorator ran first, crashing because the user wasn't authenticated yet.
The fix was to reorder decorators: @authenticate @validate ensures authentication runs first.
Rule: Decorators execute bottom-up on the same element — the one closest to the method declaration runs first.
Key Takeaway
Decorator factories run top-down; decorator application runs bottom-up (for same element type).
Property decorators execute before method decorators, before class decorators.
Always document the intended order when combining multiple decorators on a single target.

Metadata Reflection with reflect-metadata

The reflect-metadata polyfill is the backbone of decorator metadata in TypeScript. When emitDecoratorMetadata is enabled, TypeScript automatically emits metadata about the decorated element, including: - design:type — the type of the property or parameter - design:paramtypes — the parameter types of a method - design:returntype — the return type of a method

This is how frameworks like Angular and NestJS resolve dependency injection at runtime. Without the polyfill, Reflect.getMetadata throws.

Key gotcha: The metadata is collected at class definition time, not instantiation time. If a decorated class is imported but never instantiated, the metadata is still generated and stored in the global Reflect map. This can cause memory leaks if not managed.

Also, reflect-metadata must be imported as a side effect — import 'reflect-metadata' — before any decorator is evaluated. If you import it lazily or inside a module, it may not be loaded in time.

reflect-metadata-example.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'reflect-metadata';

function Injectable() {
  return function(target: any) {
    // No-op but TypeScript emits metadata because of emitDecoratorMetadata
  };
}

@Injectable()
class Service {
  constructor(private dep: OtherService) {}
}

console.log(Reflect.getMetadata('design:paramtypes', Service));
// Output: [ [Function: OtherService] ]
Missing reflect-metadata Causes Silent No-Op
Without import 'reflect-metadata', any call to Reflect.getMetadata returns undefined. Frameworks that rely on metadata (Angular, TypeORM) will fail silently — injection appears to work but properties remain null. Always import the polyfill at your application entry point before any class definition.
Production Insight
A microservice stopped injecting dependencies after a Webpack upgrade. The reflect-metadata polyfill was no longer imported early enough because of module reordering.
The fix: add import 'reflect-metadata' at the top of the entry file, before any app code.
Rule: Treat reflect-metadata as a critical polyfill with strict ordering requirements — import it first, always.
Key Takeaway
reflect-metadata is a polyfill that must be imported as a side effect before any decorator evaluation.
TypeScript automatically emits design:* metadata when emitDecoratorMetadata is true.
Missing polyfill leads to silent injection failures — the most common decorator bug in production.
● Production incidentPOST-MORTEMseverity: high

Enum Reverse Mapping Blew Up a Shared Library Bundle

Symptom
A shared TypeScript library grew to 12KB when it should have been 2KB. The entire enum object — including reverse mapping — was included in every consumer bundle, even if only one enum value was used.
Assumption
The library author assumed tree-shaking would remove unused enum values. They used a regular enum because it was simpler to debug in development.
Root cause
TypeScript's regular enum emits a var Status; (function (Status) { Status[Status["Active"] = 0] = "Active"; ... })(Status || (Status = {})); — the reverse mapping object is always generated and cannot be tree-shaken because the function invocation is a side effect.
Fix
Switch to const enum for library exports when the enum is used as a set of named constants. Const enums completely inline at compile time — no runtime overhead. For internal libraries, use plain union types or string literal types if possible.
Key lesson
  • Prefer const enum in library code to avoid bundle bloat.
  • Always check emitted JavaScript — don't assume tree-shaking handles side effects.
  • For public APIs, consider using a plain object with as const to avoid reverse mapping altogether.
Production debug guideSymptom→Action guide for the most common failures3 entries
Symptom · 01
Enum value is undefined at runtime even though the code compiles
Fix
Check if the enum is a const enum and the consumer module has isolatedModules or verbatimModuleSyntax enabled. Const enums are not inlined across module boundaries without preserveConstEnums.
Symptom · 02
Decorator function never executes; no effect on class or method
Fix
Verify that experimentalDecorators and emitDecoratorMetadata are enabled in tsconfig.json. Also confirm that reflect-metadata is imported as a side effect at the entry point (e.g., import 'reflect-metadata').
Symptom · 03
Reflect.getMetadata returns undefined for a decorated class
Fix
Check decorator factory signature — a factory must return the actual decorator function. Also confirm metadata keys match exactly (strings are case-sensitive). Use Reflect.defineMetadata explicitly if the decorator is hand-written.
★ Enum & Decorator Quick Debug Cheat SheetCommon symptoms and the exact commands to diagnose them in production or CI
Bundle includes reverse mapping when it shouldn't
Immediate action
Check emitted JavaScript: run `tsc --noEmit false --outDir ./out && grep -r -m5 "(function (" out/`
Commands
npx tsc --showConfig | grep constEnum
node -e "require('./out/index.js'); console.log(globalThis.Status)"
Fix now
Change enum Status to const enum Status and set isolatedModules: false if possible, or add constEnums: true to your tsconfig.
Decorator not applied — no metadata emitted+
Immediate action
Verify tsconfig: `tsc --showConfig | grep -E "experimentalDecorators|emitDecoratorMetadata"`
Commands
node -e "require('reflect-metadata'); console.log(Reflect.getMetadataKeys(require('./out/MyClass').default))"
grep -rn "__decorate" ./out/ --include="*.js"
Fix now
Enable experimentalDecorators: true in tsconfig. If using Angular or NestJS, ensure the reflect-metadata polyfill is imported before any class definition.
Enum value is correct in test but wrong in production build+
Immediate action
Check if the production build uses a bundler that transforms enums. Webpack's `mode: production` can override enum values via DefinePlugin.
Commands
node -e "process.env.NODE_ENV='production'; require('./dist/production.js'); console.log(Status.Pending)"
grep -r "Status.Pending" dist/ --include="*.js"
Fix now
Avoid relying on numeric enum values in production — use string enums or as const objects for values that must be stable across builds.
TypeScript Enums & Decorators Concept Comparison
ConceptUse CaseExample
Regular enumRuntime iteration, reverse mappingenum Color { Red, Green }
Const enumZero bundle overhead, internal constantsconst enum Color { Red, Green }
String enumMeaningful values, no reverse mappingenum Status { OK = 'OK' }
Decorator factoryParameterized decorators@log('info')
Metadata decoratorDependency injection, validation@Injectable() with emitDecoratorMetadata

Key takeaways

1
Regular enum compiles to an IIFE with reverse mapping
adds bundle weight that tree-shaking can't remove.
2
Const enum inlines everything but breaks with isolatedModules
use for internal code, not libraries.
3
Decorator factories run top-down; decorators apply bottom-up
order is crucial for composability.
4
reflect-metadata is a mandatory polyfill for decorator metadata
import it as a side effect before any decorated class is defined.
5
Prefer as const + union types over enums for public APIs
same safety, zero runtime cost, full tree-shaking.

Common mistakes to avoid

4 patterns
×

Memorising syntax before understanding the concept

Symptom
Developers can recite decorator factory syntax but cannot explain when or why to use one. Code reviews show decorators applied to methods that don't need them, or enums used where a union type would be simpler.
Fix
Focus on the problem first: Do I need to restrict values? Use an enum or union. Do I need to add cross-cutting behaviour without modifying the method? Use a decorator. The syntax will come with practice.
×

Skipping practice and only reading theory

Symptom
Senior engineers read this article but never open an editor. They later struggle to debug a const enum issue because they've never seen the compiled output.
Fix
Open a TypeScript playground after each section. Emit JavaScript with tsc --target ES2015 and inspect the output. Write a custom decorator and verify metadata is emitted.
×

Using regular enums in an npm library without considering bundle size

Symptom
Library consumers report unexpected bundle size increase. The enum IIFE cannot be tree-shaken away.
Fix
Use const enum and export the .d.ts. Alternatively, use a plain as const object with a type helper: const Status = { Active: 0, Inactive: 1 } as const; type Status = (typeof Status)[keyof typeof Status];
×

Forgetting to import reflect-metadata when using decorator metadata

Symptom
Decorated classes compile fine but at runtime Reflect.getMetadata returns undefined. Dependency injection fails silently.
Fix
Add import 'reflect-metadata' at the entry point before any decorator is evaluated. Ensure the import is not tree-shaken away by a bundler.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between a regular enum and a const enum in TypeSc...
Q02SENIOR
Explain the execution order of multiple decorators on a single method. H...
Q03SENIOR
What does `emitDecoratorMetadata` do, and what is the reflect-metadata p...
Q04SENIOR
How can you avoid the bundle size cost of enums in a public npm library?
Q01 of 04SENIOR

What is the difference between a regular enum and a const enum in TypeScript? When would you use each?

ANSWER
A regular enum compiles to a JavaScript object stored in an IIFE, which includes both forward and reverse mappings. It is available at runtime and can be iterated. A const enum is completely inlined — it disappears at compile time and the values are inserted as literals. Use regular enums when you need runtime iteration or reverse mapping, and when the enum is not in a library. Use const enums for internal app constants where you want zero runtime overhead and you control the build pipeline. For libraries, prefer const enum with preserveConstEnums or avoid enums entirely and use as const objects.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I use const enums in a TypeScript project with isolatedModules enabled?
02
Why is my decorator not running at all?
03
What's the difference between a method decorator and a property decorator?
04
Can enum values be computed at runtime?
🔥

That's TypeScript. Mark it forged?

5 min read · try the examples if you haven't

Previous
TypeScript with React
6 / 15 · TypeScript
Next
TypeScript Utility Types