A published TypeScript package declares `declare const enum Flag` in its .d.ts, and a consumer building with `isolatedModules` gets "Cannot access ambient const enums when 'isolatedModules' is enabled". Why does single-file transpilation make ambient const enums unusable, and what would you change?
answer
- substitution needs the declaration in scope
- one file at a time is the promise
- declare means nothing is emitted
- a local const enum has a fallback
- the ambient one has none
basics
~20 sInlining a const enum member requires reading the declaration that defines it, which is a whole-program operation. isolatedModules promises every file can be transpiled alone, and an ambient declaration emits no runtime object to fall back on, so the compiler rejects it. Ship a plain enum instead.
solid answer
~50 sReplacing `Flag.On` with `1` means the compiler must have read `Flag`'s declaration — that is cross-file knowledge. `isolatedModules` is a promise that each file can be correctly transpiled on its own, which is exactly what Babel, esbuild, swc and transpile-only pipelines actually do; such a tool sees only the importing file and has no way to learn the member's value. For a const enum declared in the *same* file, TypeScript has a safe fallback and simply emits it as an ordinary enum object instead of inlining. An **ambient** const enum has no fallback: `declare` asserts that something already exists at runtime, and for a const enum nothing ever does, so the compiler reports the error rather than emitting a reference that would be `undefined`. The fix is to stop exporting const enums from a package: publish a plain `enum`, which emits a real object every consumer can read. Keep const enums, if you want them at all, inside a compilation you control end to end.
code
typescript · 11 lines// lib.d.ts (shipped by the package) — problematic
declare const enum Flag { Off = 0, On = 1 }
// consumer.ts, compiled with --isolatedModules
// const enabled = Flag.On;
// error TS2748: Cannot access ambient const enums
// when 'isolatedModules' is enabled.
// lib.ts — the fix: a plain enum emits a real object consumers can read
export enum Flag2 { Off = 0, On = 1 }
export const enabled = Flag2.On;go deeper
Know that declare means "trust me, this already exists at runtime", and that a const enum never emits anything — so the two together leave nothing for the code to actually reference.
Explain that inlining requires reading another file's declaration, and contrast the two cases: a local const enum falls back to a real object under isolatedModules, while an ambient one has no fallback and errors.
Diagnose it from the symptom — an enum member that is undefined only under esbuild or Babel — and prescribe the fix: publish a plain enum and keep const enums inside a compilation you own end to end.
Own the API-surface rule: nothing in a published declaration may require consumers to compile with whole-program semantics, because you cannot dictate the build pipeline of every downstream team.
## Why inlining is a whole-program operation A `const enum` has no runtime representation. When the compiler sees `Flag.On`, it must look up the declaration of `Flag`, find `On`, take its value, and write that literal into the output. Every one of those steps requires the *declaration* to be in scope of whatever is doing the emit. Inside a single `tsc` invocation that is fine — `tsc` builds a program from all your files plus the `.d.ts` files of your dependencies, so it knows everything. The trouble starts when the thing producing JavaScript is not `tsc`. ## What isolatedModules actually promises The `isolatedModules` flag does not change emit for its own sake. It asks the compiler to **report anything that a single-file transpiler could not handle correctly**. It is a lint against whole-program assumptions, aimed at pipelines where each file is converted independently: Babel with its TypeScript plugin, esbuild, swc, Vite's dev transform, or a transpile-only mode used for fast test runs. Such a transpiler opens `app.ts`, sees `import { Flag } from 'lib'` and `Flag.On`, and has no mechanism to open `lib`'s declaration file and resolve a number. It can only emit a property access — which, for a const enum, refers to an object that was never emitted. The result would be a runtime `undefined` rather than a compile error, which is the worst possible failure mode. ## The two cases behave differently This is the part candidates usually miss. **A local, non-ambient const enum** under `isolatedModules` is *not* an error. TypeScript has a safe fallback: it emits the enum as an ordinary enum object and stops inlining it. ```ts // with isolatedModules enabled export const enum LogLevel { Debug = 0, Info = 1, Warn = 2 } const l = LogLevel.Warn; ``` ```js export var LogLevel; (function (LogLevel) { LogLevel[LogLevel["Debug"] = 0] = "Debug"; /* ... */ })(LogLevel || (LogLevel = {})); const l = LogLevel.Warn; // not inlined ``` You silently lose the only benefit of writing `const enum`, but the code is correct — which is the right tradeoff. **An ambient const enum** has no such fallback. `declare` means "this exists at runtime; do not emit it". For a const enum, nothing ever emits it, from any file. There is no object to fall back to, so the compiler refuses: ``` error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled. ``` Note where the error lands: on the *access*, in the consumer's code, not on the library's declaration. The library author sees nothing wrong; the consumer inherits the problem. ## What to change **Publish a plain `enum`.** This is the answer in almost every case. A plain enum emits a real object, that object ships in the package's JavaScript, and every consumer — `tsc`, Babel, esbuild, a plain JavaScript caller — can read it. You give up inlining, which was worth a few bytes. **Do not rely on `preserveConstEnums` to rescue the published case.** That flag makes *your* build emit the object, but the emitted `.d.ts` still declares a const enum, so the consumer's checker still refuses under `isolatedModules`. It solves a different problem — needing the object inside your own compilation — not the publishing problem. **Keep const enums local if you keep them at all.** Inside one project, compiled by one `tsc` invocation, with no single-file transpiler in the pipeline, a const enum is a legitimate micro-optimisation. The moment the values cross a package boundary, or the moment someone adds esbuild to the dev server, the assumption breaks. **Treat `declare const enum` as a red flag in a `.d.ts` you ship.** It bakes a whole-program requirement into your public API and forces every downstream consumer to compile the way you do. ## How to diagnose it in the wild The symptom is usually one of two shapes. Either the consumer's type-check fails with TS2748, which is loud and easy — or, worse, the consumer *is* using a raw transpiler with no type-check gate, in which case there is no error at all and the value arrives as `undefined` at runtime. When someone reports "this library's enum is undefined but only in our esbuild dev build", ambient const enums are the first thing to check: look in the package's `.d.ts` for `declare const enum`. ## The version caveat The interaction between `const enum` and `isolatedModules` has not been constant across TypeScript major versions — older compilers were stricter about local const enums than the behaviour described here. The behaviour above was verified on the TypeScript 6.0 line, so confirm against the compiler your project actually runs before quoting the exact diagnostic.
- Would turning on `preserveConstEnums` in the library's build fix the consumer's error?No. It makes the library's emitted JavaScript contain the enum object, but the shipped declaration still says `declare const enum`, and the consumer's checker rejects the access on that basis alone. The consumer's error is a type-checking decision about the declaration, not an observation about what JavaScript happens to exist. Changing the declaration to a plain `enum` is what actually fixes it.
- What happens to a non-ambient const enum declared in the same file when `isolatedModules` is on?It compiles, but it stops being inlined — the compiler emits it as an ordinary enum object and leaves the member accesses as property lookups. That is a safe fallback, because the declaration is in the same file the transpiler is looking at. The practical consequence is that you silently lose the only reason to have written `const enum`, so it is worth deleting the `const`.
- How would you spot this problem in a package whose consumers have no type-check gate in their build?You would not see a diagnostic at all — a raw transpiler emits a property access against an object that was never created, so the value surfaces as `undefined` at runtime, typically only in the build that uses the transpiler. The tell is an enum member that works under `tsc` but is `undefined` under esbuild or Babel. Grep the dependency's `.d.ts` files for `declare const enum`.
saying these in an interview costs you the question
- Says isolatedModules changes how the code executes
- Thinks preserveConstEnums fixes the consumer's error
- Believes any const enum is rejected under isolatedModules
- Assumes a declare block emits a runtime object
- Treats const enums as safe to expose from a package API