What this usually means
TypeScript enums are not just a compile-time construct: numeric enums generate a reverse mapping (value -> key) in the JavaScript output, while string enums do not. This leads to twice the expected keys when iterating over numeric enums. Const enums are completely inlined at compile time and removed from the output; if you export a const enum and another module tries to access it at runtime, you'll get undefined. Mixed enums (numeric and string members) behave differently per member type. The root cause is always a misunderstanding of how TypeScript transpiles enums to JavaScript.
The first ten minutes — establish facts before touching code.
- 1Open the compiled .js file for your enum module and inspect the object structure.
- 2Run Object.keys(MyEnum) in the browser console or Node REPL and count the keys.
- 3Check if your enum is declared as const enum; if yes, replace with regular enum and recompile.
- 4Verify enum member values: are they numbers or strings? Check with console.log(MyEnum.Foo).
- 5Look for enum usage in other modules: if using a const enum, ensure they are imported directly (not via re-export) or use preserveConstEnums.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchtsconfig.json: check 'isolatedModules' and 'preserveConstEnums' settings
- searchThe compiled JavaScript file corresponding to the enum module (e.g., myEnum.js)
- searchBrowser DevTools Sources tab: set breakpoint on enum usage and inspect the enum object
- searchNetwork tab: verify the enum module is loaded (look for 404s or wrong module paths)
- searchNode.js error stack trace: if 'undefined' appears for enum, trace the require/import chain
- searchTypeScript playground (typescriptlang.org/play) to compare transpiled output with expectations
Practical causes, not theory. These are the things you will actually find.
- warningUsing a numeric enum and iterating over it with for-in/Object.keys, getting reverse mapping entries
- warningDeclaring a const enum and then accessing it at runtime (const enums are inlined and removed)
- warningMixing numeric and string members in the same enum, causing inconsistent runtime keys
- warningExporting an enum from a barrel file (index.ts) that re-exports, losing the const enum inline
- warningUsing isolatedModules: true with const enums (they require full program context)
- warningAssuming enum values are always strings when they are numbers, leading to failed comparisons
Concrete fix directions. Pick the one that matches your root cause.
- buildFor numeric enums: avoid iterating over enum members directly; use a helper function that filters reverse mappings: Object.values(MyEnum).filter(v => typeof v === 'string')
- buildReplace const enum with regular enum if you need runtime access, or enable preserveConstEnums in tsconfig
- buildFor string enums: use them consistently; do not mix with numeric members unless intentional
- buildIf you need to iterate over enum keys, use a type-safe approach: (Object.keys(MyEnum) as Array<keyof typeof MyEnum>).filter(k => isNaN(Number(k)))
- buildExport enums directly from their defining module, not through re-exports, to avoid const enum inlining issues
- buildUse union types instead of enums if you don't need the reverse mapping or runtime object
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter fix, run Object.keys(MyEnum) and confirm the count matches the number of named members
- verifiedTest enum comparison: MyEnum.Foo === 'expectedValue' should work for string enums; for numeric, compare numbers
- verifiedCheck compiled JS: open the file and verify the enum object structure matches expectations
- verifiedRun unit tests that cover enum iteration and value access
- verifiedIn a browser app, use DevTools to inspect the enum object in the console after module load
- verifiedCheck that const enum usage in other modules compiles without errors and does not produce runtime undefined
Things that make this bug worse or harder to find.
- warningDo not assume enums are just constants; they generate runtime objects unless const enum is used
- warningDo not use for-in or Object.keys on numeric enums without filtering reverse mappings
- warningDo not switch a const enum to a regular enum without checking all imports (may increase bundle size)
- warningDo not rely on the order of enum members; reverse mappings appear first in numeric enums
- warningDo not use const enum with isolatedModules: true without understanding the limitation
- warningDo not compare enum values with '===' expecting a string when the enum is numeric
Payment Status Enum Returns 'undefined' in Production
Timeline
- 09:15Bug report: payment status check always shows 'Processing' even after completion
- 09:20Check code: PaymentStatus enum is imported and used in switch case
- 09:25Add console.log(PaymentStatus) inside switch; prints 'undefined' for Completed case
- 09:30Examine compiled enum JS: enum is defined as const enum in source
- 09:35Remove 'const' keyword from enum and rebuild; switch now works
- 09:40Compare webpack bundles: const enum was inlined in each module, causing duplication and missing import
- 09:50Revert const enum removal; instead set preserveConstEnums: true in tsconfig
- 10:00Deploy fix; payment status now correctly shows 'Completed'
I was paged about a critical payment flow bug: after users completed payment, the UI still showed 'Processing'. The status check used a TypeScript enum PaymentStatus with values like Pending, Processing, Completed. I traced the logic to a switch statement that compared the status enum. Adding a console.log inside the case for Completed printed 'undefined'. That was my first clue: the enum member wasn't resolving.
I opened the compiled JavaScript for the enum module. To my surprise, the enum was defined as a const enum in the source, and the compiled output had no enum object at all—just inline values like 0, 1, 2 scattered across modules. But the switch statement was importing PaymentStatus from a barrel file that re-exported it. The const enum was completely removed during compilation, so the import resolved to undefined at runtime.
The fix had two options: remove const to generate a runtime object, or enable preserveConstEnums to keep the object in the output. I chose preserveConstEnums because the team wanted to keep the inlining for other enums for performance. After rebuilding, the enum object existed at runtime, the switch worked, and the payment status displayed correctly. The lesson: const enums are not just an optimization; they change the runtime contract.
Root cause
const enum was used and the enum module was re-exported via a barrel file, causing the enum to be inlined and removed, leading to undefined at runtime when accessed through the import.
The fix
Set preserveConstEnums: true in tsconfig.json or removed the const keyword from the enum declaration.
The lesson
Always verify whether const enums are safe to use in your project's module bundling setup; if in doubt, use regular enums.
A numeric enum like `enum Color { Red, Green, Blue }` compiles to an object with both forward and reverse mappings: `{0: 'Red', 1: 'Green', 2: 'Blue', Red: 0, Green: 1, Blue: 2}`. This is because TypeScript assigns numeric values starting from 0 by default. The reverse mapping allows you to get the name from a value (e.g., Color[0] === 'Red'). However, this also means that Object.keys(Color) will return ['0', '1', '2', 'Red', 'Green', 'Blue']—likely not what you expect.
String enums like `enum Direction { Up = 'UP', Down = 'DOWN' }` compile to a simpler object: `{ Up: 'UP', Down: 'DOWN' }`. No reverse mapping is generated because string values are not unique identifiers. Therefore, Object.keys(Direction) returns just ['Up', 'Down']. This difference catches many developers off guard when they switch from numeric to string enums or mix them.
When you declare a `const enum`, TypeScript inlines the member values at every usage site and removes the enum declaration from the compiled output. This is a performance optimization that avoids runtime object creation. However, it has a critical constraint: const enums can only be used within the same file or in files that are part of the same compilation unit. If you export a const enum from a module and import it in another module, the import will resolve to `undefined` at runtime because the enum object was never emitted.
The problem is exacerbated when using isolatedModules: true (common with Babel or other transpilers) or when re-exporting enums through barrel files. The compiler cannot inline across module boundaries in those cases, but the const enum is still removed, leading to undefined. The safest fix is to use regular enums when you need runtime access across modules, or set preserveConstEnums: true to force emission of the enum object.
A common pattern is to iterate over enum keys or values using `Object.keys(MyEnum)` or `for...in`. For numeric enums, this yields both the numeric keys (the reverse mappings) and the string keys. This often causes logic errors when you expect only the named keys. For example, if you have a list of error codes and you iterate to display them, you'll get duplicate entries.
The fix is to filter out the reverse mappings. Since reverse mappings are numeric keys (strings that can be parsed as numbers), you can do: `Object.keys(MyEnum).filter(k => isNaN(Number(k)))`. Alternatively, use `Object.values(MyEnum).filter(v => typeof v === 'string')` to get the names. For a type-safe approach, use a helper function that returns the enum keys as an array of the enum's key type.
TypeScript allows enums to have both numeric and string members. For example: `enum Mixed { A = 1, B = 'B', C = 2 }`. The compiled output generates reverse mappings only for numeric members: `{1: 'A', 2: 'C', A: 1, B: 'B', C: 2}`. Note that 'B' does not have a reverse mapping. This inconsistency can lead to subtle bugs when iterating or accessing values. Best practice: avoid mixed enums. Stick to either all numeric or all string values.
When you see unexpected undefined or extra keys, start by checking the compiled JavaScript. Open the enum module's .js file and look for the enum definition. If it's missing, the source likely uses const enum. If it's present, inspect the object structure to see if reverse mappings exist. Next, check your tsconfig: look for isolatedModules, preserveConstEnums, and the module resolution strategy. If you're using a bundler like Webpack, check if there are multiple copies of the enum (due to inlining) using the bundle analyzer.
Another effective technique: add a runtime check in the consuming module: `console.log(MyEnum)` before using it. If it's undefined, you have a module resolution or const enum issue. If it's an object but with unexpected keys, you have a reverse mapping issue. Use `Object.entries(MyEnum)` to see all key-value pairs. Finally, replicate the scenario in the TypeScript Playground to see the exact compiled output.
Frequently asked questions
Why does Object.keys(myEnum) return twice as many entries for numeric enums?
TypeScript numeric enums generate reverse mappings: numeric values are also keys pointing back to the name. For example, enum Color { Red, Green } compiles to {0:'Red',1:'Green', Red:0, Green:1}. Object.keys returns both numeric and string keys. To get only named keys, filter with isNaN(Number(key)).
My const enum is imported but returns undefined at runtime. What's wrong?
Const enums are inlined and removed from the compiled output. If you import a const enum from another module, the import resolves to undefined because the object doesn't exist at runtime. Fix by switching to a regular enum or setting preserveConstEnums: true in tsconfig.
Can I use const enum with isolatedModules?
No, isolatedModules prevents const enums from being inlined across module boundaries because each file is compiled independently. If you must use const enums, ensure they are only used within the same file. Otherwise, use regular enums or set preserveConstEnums.
Why does my string enum member show as undefined in a switch statement?
This can happen if the enum is a const enum and the module is not properly imported. Check the compiled JavaScript: if the enum is missing, it's a const enum issue. Also ensure you are comparing with the correct value: string enums require exact string matches.
How do I iterate over enum names without getting numeric keys?
Use Object.keys(MyEnum).filter(k => isNaN(Number(k))) to get only named keys. Alternatively, define a helper function that returns the keys as a typed array. For values, use Object.values(MyEnum).filter(v => typeof v === 'string') for numeric enums.