🔷 TypeScript Q50 / 101

What is ES module interoperability?

AI-Powered Answer ✓ Answered

ES module interoperability in TypeScript refers to the ability to seamlessly import modules written in the CommonJS format (like many Node.js packages) using the ES module `import` syntax, treating them as if they had a default export, even when they don't explicitly define one.

Understanding ES Modules and CommonJS

Before modern JavaScript standardized ES Modules (import/export), Node.js primarily used CommonJS (require/module.exports). These two module systems have different conventions for exporting and importing values, particularly concerning default exports. CommonJS modules often export an object directly or a function, which ES modules would typically import as a named export or the entire module object.

The `esModuleInterop` Compiler Option

The esModuleInterop compiler option in TypeScript addresses this discrepancy. When set to true, TypeScript introduces 'synthetic default exports' for CommonJS modules. This allows you to use the more natural ES module default import syntax (import MyModule from 'my-commonjs-module';) even if the CommonJS module only exports named values or a single object via module.exports.

  • Allows import * as X from 'module' to correctly namespace all exports from a CommonJS module.
  • Enables import X from 'module' to work for CommonJS modules that do not explicitly have a default export.
  • Reduces the need for import X = require('module') syntax, promoting a more consistent ES module style.

Under the hood, when esModuleInterop is true, TypeScript generates helper code in the compiled JavaScript output (often __importDefault and __importStar functions). These helpers inspect the imported module and, if it appears to be a CommonJS module without an explicit __esModule flag, they create a synthetic default export that points to the entire module.exports object.

Example

Consider a simple CommonJS module:

javascript
/* commonjs-module.js */
module.exports = {
    a: 1,
    b: 'hello'
};

Without esModuleInterop: true in your tsconfig.json:

typescript
/* consumer.ts */
// This will typically result in a TypeScript error:
// 'Module '"./commonjs-module"' has no default export.'
import MyModule from './commonjs-module';

// You might need to import using CommonJS syntax or a workaround:
// import MyModule = require('./commonjs-module');
// OR
// import * as MyModuleNamespace from './commonjs-module';
// const MyModule = MyModuleNamespace.default || MyModuleNamespace;

With esModuleInterop: true in your tsconfig.json:

typescript
/* consumer.ts */
import MyModule from './commonjs-module';
console.log(MyModule.a); // Output: 1
console.log(MyModule.b); // Output: 'hello'

// Named imports also work for CommonJS modules in many cases:
import { a } from './commonjs-module';
console.log(a); // Output: 1

Why is it important?

It greatly improves the developer experience when working in a mixed environment where you might have ES modules alongside CommonJS modules (e.g., a modern frontend project using legacy Node.js packages). It allows developers to write code using a consistent ES module import syntax, reducing confusion and simplifying module consumption across different module systems.

Configuration

To enable ES module interoperability, set esModuleInterop to true in your tsconfig.json:

json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs", // Or "esnext", "node16", etc.
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}