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.
- 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
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.
Here's a quick look at both in TypeScript:
```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.
const enum with declaration: true and export the const enum, or use a plain object with as const.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.
- Forward mapping:
Status.Active→0 - 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.
preserveConstEnums: true removed the IIFE but kept the .d.ts for consumers.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:
- 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
tscalone. - You want the enum to be iterable (e.g., for generating UI options).
- 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.
as const objects and saved 34KB in production bundle.as const over enums — you get the same type safety with less runtime cost.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.
Example of a simple method decorator:
``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; }; } ``
@authenticate and @validate on the same method. The validation decorator ran first, crashing because the user wasn't authenticated yet.@authenticate @validate ensures authentication runs first.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.
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.import 'reflect-metadata' at the top of the entry file, before any app code.design:* metadata when emitDecoratorMetadata is true.Enum Reverse Mapping Blew Up a Shared Library Bundle
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.- 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 constto avoid reverse mapping altogether.
undefined at runtime even though the code compilesisolatedModules or verbatimModuleSyntax enabled. Const enums are not inlined across module boundaries without preserveConstEnums.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').Reflect.defineMetadata explicitly if the decorator is hand-written.enum Status to const enum Status and set isolatedModules: false if possible, or add constEnums: true to your tsconfig.Key takeaways
as const + union types over enums for public APIsCommon mistakes to avoid
4 patternsMemorising syntax before understanding the concept
Skipping practice and only reading theory
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
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
Reflect.getMetadata returns undefined. Dependency injection fails silently.import 'reflect-metadata' at the entry point before any decorator is evaluated. Ensure the import is not tree-shaken away by a bundler.Interview Questions on This Topic
What is the difference between a regular enum and a const enum in TypeScript? When would you use each?
preserveConstEnums or avoid enums entirely and use as const objects.Frequently Asked Questions
That's TypeScript. Mark it forged?
5 min read · try the examples if you haven't