ยท new features
What are ECMAScript Modules?
ECMAScript Modules (ESM) enable the importing and exporting of code and are supported in modern web browsers, Deno, Bun, and Node.js. It's recommended to use ESM as major frameworks are already embracing it. Let this tutorial guide you through the process.
ECMAScript Modules (ESM) provide a standardized approach for importing and exporting JavaScript code. This unifies the organization of code in both backend environments (such as Node.js) and frontend environments (like web browsers). Prior to ESM, different systems were used for backend and frontend development. Node.js relied on CommonJS, while the frontend world often utilized the Asynchronous Module Definition (AMD) approach.
Contents
- Key features of CommonJS
- Downsides of CommonJS
- Key features of ECMAScript Modules
- Choosing a Module Loader for Your Project
- Using ECMAScript Modules in Frontend Applications
- Using ESM with TypeScript
- Should you use ECMAScript Modules?
Key features of CommonJS
In Node.js each file, such as math.js
, is treated as an independent module. This means that the variables, functions, and code defined within math.js
are scoped to that specific module and are not directly accessible from other modules unless explicitly exported using the module.exports
syntax.
CommonJS Export Example:
To make code from one module available to another, the require()
function is being used with CommonJS.
CommonJS Import Example:
Downsides of CommonJS
CommonJS was designed for server-side development, and its syntax and module resolution were not natively supported by web browsers. CommonJS packages are loaded synchronously, which means that when you use the require()
function to import a module, it blocks the execution of the code until the required module is fully loaded. In frontend development, where asynchronous loading and non-blocking operations are crucial for performance, this synchronous nature was a significant drawback.
Key features of ECMAScript Modules
The ESM standard was established by Ecma International to bring consistency to module systems across both frontends and backends. ESM provides a modern packaging approach as it supports asynchronous loading and is natively supported in modern web browsers, Deno, Bun, and Node.js.
Using ECMAScript modules, code can be imported with relative file paths or URLs. Here's an example of how exports work in Node.js with ESM:
ESM Export Example:
Please note that the file extension was changed from .js
to .mjs
in order to signal Node.js that we are making use of the ECMAScript modules syntax instead of CommonJS. The module.exports
assignment has also been replaced using an export declaration.
ESM Import Example:
In order to import the code, we also have to change the extension of our importing file from .js
to .mjs
. Additionally, we have to switch to the import syntax:
If we don't use the correct file extension, we may see this error:
SyntaxError: Cannot use import statement outside a module
Choosing a Module Loader for Your Project
As of Node.js v13.2.0, you can include "type": "module"
in your package.json
configuration to enable the ECMAScript module loader. By doing so, there's no need to modify all the file extensions from .js
to .mjs
. To enforce the use of CommonJS for certain files, you can employ the .cjs
extension.
Vice versa, you use "type": "commonjs"
in your package.json
file to utilize CommonJS. You can then apply the .mjs
extension to treat individual files with the ECMAScript module loader.
Using ECMAScript Modules in Frontend Applications
Importing an ESM module into a website is simple. Similar to setting "type": "module"
in a Node.js application, you can define type="module"
within a <script>
tag. Following that, you can use ESM's import
syntax and start using the imported code:
Using ESM with TypeScript
TypeScript does a fantastic job in supporting CommonJS and ESM. It allows developers to define module resolution and export strategies for their own code. If you are already making use of the import
and export
syntax, then you can easily switch to ESM. Simply change the values for module and moduleResolution in your tsconfig.json
file.
The "moduleResolution"
setting defines how you import code and the "module"
setting defines how you export code. With the correctness fixes in TypeScript 5.2 these two settings are very much correleated to another. That's why it's advisable to keep them in sync.
Module Values:
node10
(previouslynode
): This has to be set for Node.js environments that only support CommonJS (module.exports
&require
)node16
: This is a stable setting if you want to support ESM or CommonJS (depending on your file extension or the"type"
in yourpackage.json
file)nodenext
: This is a nightly build setting and will use the latest module resolution features (recommended if you want to use experimental features such as JSON import assertions)esnext
: This is a nightly build setting (similar tonodenext
) for frontend applications
It's sufficient to only set the value for "module"
as it will affect the "moduleResolution"
. Only if you want them to be different, you need to specify them indivually. The esModuleInterop option is also coupled to the "module"
setting and is already set to true
if using node16
or nodenext
.
ESM Imports & Exports
ECMAScript modules require explicit file extensions in import and export statements. Add a .js
extension to your code. Yes, you read it right - even when using TypeScript with .ts
files, mark the imports with .js
. The TypeScript compiler will handle this seamlessly.
Before:
After:
Pro tip: You can skip the manual work by using a code conversion tool like TS2ESM.
Module File Extensions
Just as the .mjs
and .cjs
file extensions are introduced, TypeScript brings forth the .mts
and .cts
extensions, accompanied by their type definition extensions .d.mts
and .d.cts
. But don't worry, if you specify your module format in your package.json
file using the "module"
setting, you can still use plain .ts
files without any problems.
Recommendation for TypeScript Backends
When working on code for Node.js environments, it's recommended to use node16
as it provides stability. This setting is also recommended by the TypeScript team for Node v18-20 (see comment here). If you need to import JSON files, make sure to set the "module"
to nodenext
, as JSON import assertions won't be accessible otherwise.
Recommendation for TypeScript Frontends
When using an external bundler such as esbuild, it is recommended to set "module"
to esnext
and "moduleResolution"
to bundler
. The bundler
setting eliminates the need for file extensions on relative paths.
Templates for TS configs
To save time and effort, you can use configuration templates. The TypeScript maintainers and alumni have published reusable config templates that can be used with the extends option. Just install the base templates and extend the desired configuration:
Frameworks, such as Astro, also offer configurations that work seamlessly out-of-the-box:
Should you use ECMAScript Modules?
Absolutely! The Ecma International group is the powerhouse behind standardizing JavaScript and introducing ECMAScript modules as THE way to import and export code. It's crystal clear that this will sweep away previous solutions, with major frameworks already embracing the change.
A few examples:
- Prettier ships ESM standalone bundles
- Deno is advocating ES modules
- Jest works on native support for ES modules
- Tailwind CSS can be configured in ESM
- TypeScript's development team has rebuilt its codebase to use ECMAScript modules
- Vitest has fully-fledged ESM support
- Redux maintainer Mark Erikson gives a handful of advices modernizing packages to ESM
However, if transitioning your code to ESM isn't feasible at the moment, there are tools available to assist you. Isaac Z. Schlueter, the creator of npm, published a tool called tshy for this purpose, enabling you to distribute your code as hybrid modules, compatible with both CommonJS and ES modules.
Andrew Branch, who is working on TypeScript at Microsoft, developed a solution called "Are the types wrong?". It provides a website and CLI to examine contents of npm packages to identify issues with their TypeScript types, with a focus on errors related to ESM module resolution.