ESM, or ECMAScript Modules, is the official module system introduced in ECMAScript 6 (ES6), also known as ECMAScript 2015. ESM provides a native, standardized way to organize and structure code in JavaScript applications. Unlike CommonJS, which is synchronous and primarily used in Node.js, ESM is designed to work in both browser and server-side environments.
Asynchronous Loading
One of the significant differences from CommonJS is that ESM is designed for asynchronous loading. This makes it suitable for modern, performance-oriented web development, where asynchronous loading is preferred to avoid blocking the main thread.
Browser and Node.js Support
ESM is supported natively in modern browsers and in recent versions of Node.js. This native support simplifies the development process, eliminating the need for transpilers or bundlers in many cases.
Static Analysis
ESM supports static analysis, meaning that dependencies are determined at compile time rather than runtime. This allows tools to optimize the loading of modules.
Encapsulation
Everything inside an ES6 module is private by default, and runs in strict mode (there’s no need for 'use strict'
).
Circular Dependencies
ESM handles circular dependencies more effectively than CommonJS. Circular dependencies occur when two or more modules depend on each other.
Syntax for JavaScript and TypeScript is the same:
// Exporting a variable or function export const myVariable = "Hello, ESM!"; // Exporting a class export class MyClass { /*...*/ } // Exporting a function export function myFunction() { /*...*/ }
Importing
// Importing a named export import { myVariable, myFunction } from './myModule'; // Importing a default export import MyDefaultExport from './myModule';
More syntax variations of import
.
import defaultExport from "module-name"; import * as name from "module-name"; import { export1 } from "module-name"; import { export1 as alias1 } from "module-name"; import { default as alias } from "module-name"; import { export1, export2 } from "module-name"; import { export1, export2 as alias2, /* … */ } from "module-name"; import { "string name" as alias } from "module-name"; import defaultExport, { export1, /* … */ } from "module-name"; import defaultExport, * as name from "module-name"; import "module-name";
Dynamic Imports
- ESM introduces the
import()
function for dynamic imports. This allows you to load modules asynchronously, improving performance by loading only the necessary modules when they are needed.
// Dynamic import import('./myModule').then((module) => { // Use the module });
Usage in browsers
ESM is considered the modern standard for JavaScript modules, and its native support in both browsers and Node.js makes it a versatile and powerful choice for organizing and structuring code in contemporary JavaScript applications.
In order to use the import
declaration in a source file, the file must be interpreted by the runtime as a module.
When you are using ECMAScript Modules (ESM) in a browser environment, you need to specify the type="module"
attribute in the script tag to indicate that the script should be treated as an ECMAScript module. This is necessary because the default script type is assumed to be JavaScript (non-module).
<!-- Using ESM in the browser --> <script type="module" src="main.js"></script>
When you are using ECMAScript Modules (ESM) in a browser environment, the server should serve the module files with the MIME type application/javascript
. This ensures that the browser correctly interprets the file as JavaScript code.
Here’s an example of how the server might set the MIME type for JavaScript files:
Content-Type: application/javascript
There is a difference in how regular <script>
tags and ECMAScript Modules (ESM) handle cross-origin requests and adhere to the Same-Origin Policy.
Regular <script>
Tags:
- Traditional
<script>
tags do not enforce the Same-Origin Policy. They can fetch scripts from other domains without restrictions. This behavior is known as script-src cross-origin loading.
<!-- Regular script tag fetching a script from another domain --> <script src="https://example.com/script.js"></script>
- This lack of restriction for regular
<script>
tags can pose security risks, as it allows scripts from external domains to be executed on a web page without proper control.
ECMAScript Modules (ESM):
- ECMAScript Modules, on the other hand, are subject to the Same-Origin Policy. When you use the
import
statement to fetch a module, the browser enforces CORS (Cross-Origin Resource Sharing) policies. - If the module is on a different domain, the server hosting the module needs to include the appropriate CORS headers to allow the cross-origin request. Without proper CORS headers, the browser will block the request.
<!-- Module script tag fetching a module from another domain --> <script type="module" src="https://example.com/module.js"></script>
This requires CORS to be set correctly on the server. The following example CORS header allows any origin to access the resource:
Access-Control-Allow-Origin: *
Modules won’t send cookies or other header credentials unless a crossorigin="use-credentials"
attribute is added to the <script>
tag and the response contains the header Access-Control-Allow-Credentials: true
.
ES modules in Node.js
In Node.js, you can use ECMAScript Modules (ESM) by following one of the methods:
You can specify that your project is using ESM by adding a “type” field with the value “module” to your package.json
file. With this configuration, all .js
files in your project are treated as ESM.
// package.json { "type": "module", "main": "main.js" }
Alternatively, you can use the .mjs
file extension for your ESM files. Files with this extension are treated as ESM by default.
// main.mjs import { myFunction } from './myModule'; myFunction();
When running a script with Node.js, you can explicitly specify the input type as “module” using the --input-type
flag. This is useful if you want to run a specific script as an ESM module without modifying the package.json
or file extensions.
node --input-type=module myScript.js
The “type”: “module” approach in package.json
is commonly used as it provides a project-wide setting, while the .mjs
extension is useful for individual files. The --input-type
flag allows for on-the-fly specification when running scripts.
Common errors
The following error indicates that you are attempting to use the require()
statement to import an ECMAScript Module (ESM) in a context where CommonJS is expected. This error typically occurs when there is a mix of module types in your Node.js project, so if you are using ESM in your project (e.g., by setting "type": "module"
in package.json
or using .mjs
file extension), you should consistently use import
and export
syntax throughout your codebase.
(node:26444) [ERR_REQUIRE_ESM] Error Plugin: xyz [ERR_REQUIRE_ESM]: require() of ES Module C:\code\xyz\node_modules\somepackage\lib\somepackage.js from C:\code\xyz\dist\index.js not supported. Instead change the require of somepackage.js in C:\code\xyz\dist\index.js to a dynamic import() which is available in all CommonJS modules.
Here’s an example of how this error might occur:
// somepackage.js - ESM module export const someFunction = () => { console.log('This is an ESM module.'); };
Next, we make the mistake of using require to import ESM module in CommonJS module:
// index.js - CommonJS module const somePackage = require('somepackage'); // This line would trigger the error somePackage.someFunction();
To fix this either ensure that your entire codebase uses the same module type: Either convert your project to CommonJS by removing "type": "module"
from package.json
(or renaming .mjs
files to .js
), or convert the entire project to ESM by ensuring that all files use import
and export
syntax.
If you want to keep the mixed module types, you can use dynamic import to load the ESM module:
// index.js - CommonJS module const somePackage = await import('somepackage'); somePackage.someFunction();
Error: Cannot use import statement outside a module
Cannot use import statement outside a module
This error mainly occurs when you use the import
keyword to import a module in a file that Node.js does not recognize as an ECMAScript module. Or when you omit the type="module"
attribute in a script
tag.
- If your project is intended to use ESM but lacks the
"type": "module"
field inpackage.json
, Node.js will treat files with.js
extensions as CommonJS by default. - To use ESM, either change the extension to
.mjs
or set"type": "module"
.
CommonJS vs. ECMAScript modules
CommonJS and ES6 modules cannot be mixed together, because apart from the different syntax for importing and exporting, CommonJS and ES6 modules execute code at different times when code is being imported: ES6 modules are pre-parsed in order to resolve further imports before code is executed. CommonJS modules load dependencies on demand while executing the code.
// CommonJS modules // --------------------------------- // one.js console.log('running one.js'); const hello = require('./two.js'); console.log(hello); // --------------------------------- // two.js console.log('running two.js'); module.exports = 'Hello from two.js';
running one.js running two.js hello from two.js
But with ES2015
// ES2015 modules // --------------------------------- // one.js console.log('running one.js'); import { hello } from './two.js'; console.log(hello); // --------------------------------- // two.js console.log('running two.js'); export const hello = 'Hello from two.js';
running two.js running one.js hello from two.js
ESM is natively supported by Node.js 12 and later.
ES modules in TypeScript
To set up a TypeScript project to use ECMAScript Modules (ESM), you need to configure TypeScript to generate code compatible with ESM and also ensure that Node.js recognizes your project as an ESM project. Here are the steps to set up a TypeScript project with ESM:
npm install -g typescript npm install --save-dev typescript ts-node
// tsconfig.json { "compilerOptions": { "module": "ESNext", "target": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "strict": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }
"module": "ESNext"
: Specifies that TypeScript should generate ESM code."target": "ESNext"
: Specifies the ECMAScript version that TypeScript will generate."moduleResolution": "node"
: Enables Node.js module resolution.
Create a simple TypeScript file in a src
directory:
// src/main.ts export const greet = (name: string): string => { return `Hello, ${name}!`; }; console.log(greet('World'));
Run TypeScript Code using ts-node with npm start
:
// package.json { "scripts": { "start": "ts-node src/main.ts" } }
If you are using Node.js directly to run your code, ensure to use:
node --experimental-specifier-resolution=node dist/main.js
TypeScript compilerOptions and modules
compilerOption esModuleInterop explained
"esModuleInterop" : "true"
: addresses situations where you are using both ESM and CommonJS modules in the same project and need to seamlessly import and use modules from both systems. If set to true, you can use import
directly, and TypeScript will handle the necessary adjustments:
// ESM with esModuleInterop enabled import myModule from './myModule';
If set to false, you may need to use the more verbose syntax for importing CommonJS modules with default exports
// ESM without esModuleInterop import * as myModule from './myModule'; const defaultValue = myModule.default;
esModuleInterop
is especially useful when you are working with existing code or third-party libraries that are written in CommonJS but you want to use them in a project that primarily uses ESM.
compilerOption module explained
module specifies what JS code tsc will emit:
If the input source code is:
import { createSourceFile } from "typescript"
then module: “commonjs” will emit
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const typescript_1 = require("typescript");
and module: “esnext” will emit
import { createSourceFile } from "typescript"
You cannot write imports in the style of import fs = require("fs")
under --module es2015
(or higher ES targets) because there is no require
to speak of in the ES module system.
Additionally, using top-level await
is only allowed in --module es2022
(or higher) or system
because it requires corresponding support in the module loading system.
Specifying module: “nodenext” implies that moduleResolution is set to nodenext (a new resolution mode designed for Node’s specific implementation of co-existing ESM and CJS).
compilerOption moduleResolution explained
This defines the algorithm that answers the question where and how to look for a module. Looking in a folder “node_modules” is related to Node but also many other non-Node code has adopted this location for convenience, but is is not going to be correct for every runtime.
Using nodenext requires that your file endings are either mts or mjs for ESM and cts or cjs for CommonJS.
Choosing between CommonJS and ESM for Node.js projects
Both module systems have their advantages and trade-offs, and the choice often depends on factors like project requirements, ecosystem compatibility, and personal or team preferences. Here are some reasons why you might opt for CommonJS:
- Ecosystem Compatibility:
- The Node.js ecosystem has traditionally used CommonJS modules, and many existing npm packages are written in CommonJS. If your project heavily depends on existing npm modules that are not yet converted to ESM or are not compatible with ESM, you might choose to stick with CommonJS.
- Tooling and Libraries:
- Some tools and libraries in the Node.js ecosystem are designed with CommonJS in mind. If you rely on specific tools or libraries that work seamlessly with CommonJS, it might be easier to start your project with CommonJS to avoid potential compatibility issues.
- Incremental Adoption:
- If you are working on an existing project or transitioning from an older version of Node.js that primarily uses CommonJS, you might choose to adopt ESM gradually. Starting with CommonJS allows for an incremental transition, enabling you to update individual modules to ESM as needed.
- Maturity and Stability:
- CommonJS has been a stable and widely used module system in Node.js for many years. It has a mature ecosystem, and developers are familiar with its conventions. If stability and a well-established ecosystem are crucial for your project, starting with CommonJS might be a pragmatic choice.
- Simplicity and Familiarity:
- CommonJS syntax is simpler and might be more familiar to developers who have been working with Node.js for an extended period. If your team values simplicity and ease of use, and you don’t need the advanced features introduced by ESM, CommonJS might be a straightforward choice.
- Tooling Support:
- While tooling support for ESM is improving, CommonJS is still more widely supported by various development tools, including bundlers, transpilers, and testing frameworks. If you rely on a specific set of tools that work well with CommonJS, it can influence your choice.
It’s essential to note that Node.js itself fully supports both CommonJS and ESM, and the choice between them often depends on project-specific considerations. Additionally, Node.js continues to evolve, and the ecosystem is gradually adapting to embrace ESM more widely. When starting a new project, it’s a good idea to evaluate the specific requirements and constraints and choose the module system that aligns best with your needs.
TS2354: This syntax requires an imported helper but module tslib cannot be found.
I came across this error when using async / await in ts file module and tsconfig.json like this:
"compilerOptions": { "module": "ESNext", "moduleResolution": "esnext",
tslib
is commonly used to support certain JavaScript features, including the transformation of async
/await
code.
Try adding:
npm install tslib
And this ensures that TypeScript includes the necessary library for supporting async
/await
:
// tsconfig.json { "compilerOptions": { "lib": ["esnext", "other-libraries"], // other options... }, // other settings... }
Alternatively, the error disappears after I switched to
"compilerOptions": { "module": "NodeNext", "moduleResolution": "nodenext",