Transpiled CommonJS output often begins with `Object.defineProperty(exports, "__esModule", { value: true })`. What is that marker for, and how do interop helpers such as Babel's `_interopRequireDefault` or the `__importDefault` helper emitted by TypeScript use it?
answer
- a flag, not a language feature
- disambiguates one exports shape
- three lines decide wrap or pass through
- written non-enumerably, on purpose
- not every loader agrees to read it
basics
~20 sIt is a tooling convention marking a CommonJS exports object as lowered ES module output, so its default property is a real default export. Interop helpers read it to decide whether to pass the object through or wrap it as { default: value }.
solid answer
~40 s`__esModule` answers one question for a consumer: is this exports object hand-written CommonJS, or is it an ES module that a compiler flattened into CommonJS? That matters because compilers encode a default export as `exports.default`, and a consumer reading `.default` needs to know whether that key is a genuine default export or just a property that happens to be named `default`. The helper is three lines: `obj && obj.__esModule ? obj : { default: obj }`. Marker present, trust the existing `.default`; marker absent, treat the whole exports value as the default and wrap it. It is purely a convention — no ECMAScript specification mentions it, and Node's native ESM loader ignores it entirely, which is why the same package can hand you a function under a bundler and a wrapper object under Node.
go deeper
Recognise the marker on sight and know it means the file is compiled ES module output, so its default export lives on the default property rather than being the exports object itself.
Write the interop helper from memory and explain both branches, including why an already-marked object must be passed through untouched instead of wrapped a second time.
Use the divergence diagnostically: bundlers honour the marker and Node's native loader does not, so the same import can bind different values in a bundled build and a plain Node run of the same source.
Treat it as an unspecified convention your builds depend on. Decide how many interop layers a dependency may pass through, and prefer packages publishing real ESM entry points over ones relying on the marker to be interpreted correctly.
## The problem the marker solves CommonJS has exactly one export slot: `module.exports`. ES modules have named exports plus a distinguished `default` export. When a compiler lowers ESM to CommonJS, it flattens the two-slot model into the one-slot model by convention: ```js // export const a = 1; export default fn; Object.defineProperty(exports, '__esModule', { value: true }); exports.a = 1; exports.default = fn; ``` That encoding is lossy in one specific way. A consumer looking at `{ default: something }` cannot tell whether it is reading compiled ESM output whose default export is `something`, or a hand-written CommonJS module that legitimately exports an object with a property called `default`. The marker removes the ambiguity: it says "this object is compiler output; interpret the `default` key as a default export." ## How it is written Always with `Object.defineProperty`, never `exports.__esModule = true`. The reason is enumerability. A property defined through a descriptor without `enumerable: true` is non-enumerable, so it is skipped by `Object.keys`, `for...in`, object spread and `JSON.stringify`. That keeps the marker out of anything that copies or serialises the exports object — a fake export named `__esModule` leaking into a namespace copy would be a genuine bug. Babel emits exactly that call. The TypeScript compiler emits an equivalent under its `__createBinding`/`__exportStar` helper family, and bundlers emit their own variants when they produce CommonJS output. ## How the helper reads it The consuming half of the convention is a wrapper injected around every `require()` that feeds a default import: ```js function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } ``` The TypeScript emit is the same logic named `__importDefault`. Both branches matter: - **Marker present** — pass through. The object already has a meaningful `.default`; wrapping again would produce `{ default: { __esModule: true, default: fn } }` and bury the value one level deeper. That is the origin of the occasional `pkg.default.default` you see when two interop layers stack. - **Marker absent** — wrap. The module is hand-written CommonJS, so the only sensible candidate for "the default export" is the entire `module.exports` value. `{ default: obj }` makes the downstream `.default` read land on it. The `obj &&` guard is not decorative: `require()` can legitimately return `null` or `undefined` if a module assigns those to `module.exports`, and property access on them would throw. There is a wider sibling for namespace imports — `_interopRequireWildcard` / `__importStar` — which copies own enumerable properties onto a fresh object and sets `default` to the whole exports value, approximating a module namespace object. ## The crucial limit: nobody is obliged to honour it `__esModule` appears in no specification. It is a de facto convention among build tools, and the consumers that respect it are exactly the ones that emit it. Concretely: - **Compiled CommonJS consumers** honour it — that is the whole point. - **Bundlers** generally honour it when resolving a default import of a CommonJS dependency, so a compiled package's default import gives you the inner function. - **Node's native ESM loader does not.** Its rule is unconditional: the default export of a CommonJS module is `module.exports`. If `module.exports` is `{ __esModule: true, default: fn }`, then `import pkg from '...'` binds that entire object and the function is at `pkg.default`. That divergence is the practical takeaway. The same `import fn from 'compiled-pkg'` statement can bind a function under a bundler and a wrapper object under plain `node app.mjs`. Code that works in a bundled build and throws `fn is not a function` in a Node-executed script is very often this exact divergence rather than a version skew. ```js // compiled-pkg's module.exports === { __esModule: true, default: greet } // under a bundler honouring the marker: greet // under Node's native ESM loader: { __esModule: true, default: greet } ``` ## What to say in an interview Name it as a convention, not a language feature; recite the three-line helper; state both branches and why re-wrapping compiled output is harmful; and finish with the divergence — bundlers read the marker, Node does not. That last point is what separates someone who has read about interop from someone who has debugged it.
- What goes wrong if an interop helper wraps an object that already carries the marker?You get `{ default: { __esModule: true, default: fn } }`, so the use site's `.default` read yields the inner exports object instead of the function and you need `.default.default` to reach it. That is precisely why the helper's first branch passes marked objects through untouched, and why stacked interop layers produce the double-default symptom.
- Why is the marker defined with `Object.defineProperty` rather than a plain assignment?A descriptor without `enumerable: true` produces a non-enumerable property, so the marker is invisible to `Object.keys`, `for...in`, spread and `JSON.stringify`. A plain assignment would make it enumerable, and it would then leak into any code that copies or serialises the exports object — showing up as a bogus export named `__esModule`.
- If the marker is only a convention, why hasn't Node's ESM loader adopted it?Node's interop rule has to be total and predictable for every CommonJS module ever published, not just compiler output. Making the default export conditional on an unspecified property would mean a package could change what `import pkg from` binds by adding a property, and would make a hand-written module that genuinely exports a `default` key ambiguous. Node chose the unconditional rule instead.
saying these in an interview costs you the question
- Calls __esModule part of the ECMAScript specification
- Says Node's ESM loader checks the marker
- Thinks the helper strips .default rather than adding it
- Claims every CommonJS module carries the marker
- Ignores that re-wrapping marked output buries the value