Title: `esModuleInterop` and `allowSyntheticDefaultImports` in TypeScript 6.0+ · Issue #62529 · microsoft/TypeScript · GitHub
Open Graph Title: `esModuleInterop` and `allowSyntheticDefaultImports` in TypeScript 6.0+ · Issue #62529 · microsoft/TypeScript
X Title: `esModuleInterop` and `allowSyntheticDefaultImports` in TypeScript 6.0+ · Issue #62529 · microsoft/TypeScript
Description: Acknowledgement I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion. Comment Related: #54500 https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-inter...
Open Graph Description: Acknowledgement I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion. Comment Related: #54500 https://www.typescriptlang.org/docs/h...
X Description: Acknowledgement I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion. Comment Related: #54500 https://www.typescriptlang.org/do...
Opengraph URL: https://github.com/microsoft/TypeScript/issues/62529
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"`esModuleInterop` and `allowSyntheticDefaultImports` in TypeScript 6.0+","articleBody":"### Acknowledgement\n\n- [x] I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.\n\n### Comment\n\nRelated:\n- #54500\n- https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html\n\n## Background\n\nThese options come into play when compiling\n\n```ts\nimport x from \"./transpiled-cjs\";\nconsole.log(x);\n```\n\nto CommonJS, where `./transpiled-cjs` is also a CommonJS module. With both options off, the emit of the above is:\n\n```js\nconst transpiled_cjs_1 = require(\"./transpiled-cjs\");\nconsole.log(transpiled_cjs_1.default);\n```\n\nIf `\"./transpiled-cjs\"` was compiled from ESM syntax with a default export, this works perfectly—a default import resolves to a default export. But now, let’s say we install an npm package that exposes the following CommonJS module:\n\n```js\nmodule.exports = function sayHello() {\n console.log(\"Hello\");\n};\n```\n\nHow are we supposed to import this? It has no `default` property, so `cjs_1.default` is `undefined`. We could perhaps use a namespace import:\n\n```ts\nimport * as x from \"cjs-pkg\";\n```\n\nbecause it compiles to\n\n```js\nconst x = require(\"cjs-pkg\");\n```\n\nWhile this works at runtime, it breaks ESM semantics, because a namespace import in real ESM always resolves to a namespace-like object with no signatures. If we want to be able to use a CommonJS module that defines a single, non-namespacey `module.exports` from a transpiled ES module, it seems like we _have_ to be able to use a default import for it. So in summary, a transpiled default import\n\n1. needs to resolve to the `module.exports` of “true” CJS modules, and\n2. needs to resolve to the `module.exports.default` of ESM-transpiled-to-CJS modules.\n\nTo achieve this, the JavaScript community converged long ago on the convention of defining an `module.exports.__esModule` property in files that were transpiled from ESM to CJS:\n\n```js\nObject.defineProperty(module.exports, \"__esModule\", { value: true });\nexports.default = 42;\n```\n\nThis allows compilers to implement a default import that checks the `__esModule` flag in order to determine whether to return `module.exports` or `module.exports.default`:\n\n```js\nvar __importDefault = (this \u0026\u0026 this.__importDefault) || function (mod) {\n return (mod \u0026\u0026 mod.__esModule) ? mod : { \"default\": mod };\n};\nconst transpiled_cjs_1 = __importDefault(require(\"./transpiled-cjs\"));\nconsole.log(transpiled_cjs_1.default);\n```\n\n**This improved emit is the primary thing `esModuleInterop` does.** The option `allowSyntheticDefaultImports` goes hand in hand with it—when enabled, the type checker is aware that _sometimes_ a default import can resolve to `module.exports`, and will try to figure out whether that’s the case by looking at the types of the module being imported.\n\n```ts\nimport x from \"cjs-pkg\";\n// with allowSyntheticDefaultImports, the compiler will resolve types for cjs-pkg\n// and try to guess whether the JS file has `__esModule` defined or not. In other words,\n//\n// return (mod \u0026\u0026 mod.__esModule) ? mod : { \"default\": mod };\n// ^^^ ^^^^^^^^^^^^^^^^^^\n// will we take this branch ^ at runtime, or ^ this one?\n```\n\n## Some sweeping conclusions based on these facts\n\n1. **`allowSyntheticDefaultImports` without `esModuleInterop` or vice versa should never have been allowed.** `allowSyntheticDefaultImports` came first in order to support type checking behavior that would reflect the runtime behavior of Babel and Webpack. When `esModuleInterop` was added later, it implied `allowSyntheticDefaultImports`, but each option was still allowed to be set independently.\n2. **`esModuleInterop` should not be an option; it should be our standard emit.** The JavaScript ecosystem has universally agreed that an ESM default import resolves to `module.exports` for true CJS modules.\n3. **Correct type checking behavior depends on accurately determining whether the target module is a CJS module with `__esModule`.** Because TypeScript usually analyzes declaration files instead of the JavaScript that will get loaded at runtime, it’s possible for us to be misled on both whether a module is ESM or CJS and whether it has `__esModule`.\n\n## Guessing whether a module has an `__esModule` marker\n\nWhen we’re misled about whether a declaration file represents an ES module or CJS module, that’s always the result of a configuration error of the kind that https://arethetypeswrong.github.io was made to diagnose and fix. But when we’re trying to figure out whether a CJS module has an `__esModule` marker based on its declaration file, there is some ambiguity of our own making. These are the current rules for determining whether a module was transpiled from ESM syntax (i.e., whether it has an `__esModule` marker):\n\n1. **If the file we’re looking at is actually JavaScript, we can just look for `__esModule`.**\n2. **If the file is a `.ts` (non-declaration) file, we know we’re going to emit `__esModule` to the JavaScript unless it uses `export =`.**\n3. **If a declaration file has an `export default`, it is assumed to be compiled from ESM.** This is a heuristic, but a decently good one—it holds for any `.d.ts`/`.ts` pair that was generated by us. It could go wrong if a hand-written declaration file uses `export default` to describe a `module.exports.default` member of a true CJS module, but that case is fairly rare—it merits being documented in DefinitelyTyped guidance if it’s not already.\n4. **If a declaration file explicitly declares `__esModule` in the typings, the JS is assumed to have it.** I’ve never seen this, but it makes sense.\n\nOtherwise, we assume the module is a true CJS module without `__esModule`. This is where the real ambiguity lies. A TypeScript file like\n\n```ts\nexport const x = 42;\nexport const y = 43;\n```\n\nwill emit as\n\n```ts\n// .js\n\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.x = 42;\nexports.y = 43;\n\n// .d.ts\nexport declare const x = 42;\nexport declare const y = 43;\n```\n\nIf these compilation outputs are imported into another project using `esModuleInterop`, the rules stated above result in the compiler assuming that the JS **does not have an `__esModule` marker**, thereby incorrectly allowing a default import:\n\n```ts\nimport mod from \"x-and-y\";\n// ^^^ binds to `module.exports.default` (undefined) at runtime,\n// but `module.exports` in the type system!\nconsole.log(mod.x, mod.y); // 💥 TypeError: Cannot read properties of undefined (reading 'x')\n```\n\nIn other words, when we don’t know whether a default import is valid or not, we silently allow it in order to be more permissive. The only way to plug this hole is to stop using `esModuleInterop`/`allowSyntheticDefaultImports`, but that also prohibits default imports when we _definitely know_ they would work.\n\n## Proposed solutions\n\n1. **Deprecate `esModuleInterop: false`.**\n2. In addition, do one of the following with `allowSyntheticDefaultImports`:\n 1. Deprecate it too, making it always-on. Do nothing about the type-checking hole described above.\n 1. Change `allowSyntheticDefaultImports` to mean “allow default imports _when it’s ambiguous whether they’re safe_.” Default imports that are definitely (or almost definitely, based on the heuristics above) safe would always be allowed. The option would essentially become a strictness option (with `false` being stricter than `true`).\n 1. Deprecate it and make ambiguously-safe default imports always an error. It could catch some false positives on hand-authored declaration files, but using named imports, a namespace import, or `import x = require(\"mod\")` are all viable workarounds on the importer side, and we could make a mass change to DefinitelyTyped to fix the most common false positives on the exporter side:\n ```diff\n - export declare const x: number;\n - export declare const y: number;\n + declare namespace mod {\n + const x: number;\n + const y: number;\n + }\n + export = mod;\n ```\n This declaration file style is currently disallowed by a longstanding and arguably misguided lint rule in DefinitelyTyped, but we could make this the new preference for disambiguating transpiled CJS from true CJS along with an automated one-time bulk migration.\n\nMy preference: test (iii) on top repos and consider. I’m torn between (i) and (ii) otherwise, as I think the type checking hole is unfortunate, but it’s also longstanding and doesn’t get many complaints, so it would be a shame for it to stand in the way of deprecating an option with a name as bad as `allowSyntheticDefaultImports`.","author":{"url":"https://github.com/andrewbranch","@type":"Person","name":"andrewbranch"},"datePublished":"2025-10-02T18:23:12.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":1},"url":"https://github.com/62529/TypeScript/issues/62529"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:76d314bd-de3f-9d06-5578-a10bdd1a712c |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | C23A:BCEAA:FDAE15:15BD2D9:6A62D6F1 |
| html-safe-nonce | fb9268e7765efcdc6b66e238a421f8dbf4759302d85b8ba0219333b9525f72fc |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDMjNBOkJDRUFBOkZEQUUxNToxNUJEMkQ5OjZBNjJENkYxIiwidmlzaXRvcl9pZCI6IjkwMTgyNzkyNDEzNTczODM0MDkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 5fcc364b49698ca29d15dae2cf19e9547da6d93078ffbba0337886a4664fd16b |
| hovercard-subject-tag | issue:3478485085 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/microsoft/TypeScript/62529/issue_layout |
| twitter:image | https://opengraph.githubassets.com/0334259e0986291965618c02de2b1b7a3fcaf346a32c7c92791cf6d521adf5d5/microsoft/TypeScript/issues/62529 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/0334259e0986291965618c02de2b1b7a3fcaf346a32c7c92791cf6d521adf5d5/microsoft/TypeScript/issues/62529 |
| og:image:alt | Acknowledgement I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion. Comment Related: #54500 https://www.typescriptlang.org/docs/h... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | andrewbranch |
| hostname | github.com |
| expected-hostname | github.com |
| None | df33b1b61ee7b9a0af988199bfc3503c9c1acafb1f1d40e1f140ea7c84f890dd |
| turbo-cache-control | no-preview |
| go-import | github.com/microsoft/TypeScript git https://github.com/microsoft/TypeScript.git |
| octolytics-dimension-user_id | 6154722 |
| octolytics-dimension-user_login | microsoft |
| octolytics-dimension-repository_id | 20929025 |
| octolytics-dimension-repository_nwo | microsoft/TypeScript |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 20929025 |
| octolytics-dimension-repository_network_root_nwo | microsoft/TypeScript |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | d41cd1bdb290013455c0ac430fa755621733f5eb |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width