Skip to main content

Migrating to ESM

· 3 min read
Homa Wong (unional)

ESM, or ECMAScript Module, was original part of the ES6/ES2015 specification. It turned out to be difficult to implement and was pulled from the core specification at the last minute.

There were many discussions and argument about it. At one point, it almost falls apart because NodeJS couldn't get to a good design to get CommonJS and ESM to co-exist.

Fast-forward a few years, ESM is mature enough to be used on both NodeJS and browsers.

By now, I'm pretty sure you have heard of it already.

But for people who write code in TypeScript, while we use import and export for a very long time, we were not able to publish the code as ESM.

What's worse is that, some JavaScript code moved on to ESM and the new module package in NodeJS, and TypeScript code cannot consume them.

So the TypeScript code is being left behind.

TypeScript 4.5 planned to support the new module system, but the support was pulled due to some cases were not resolved.

While it was heartbreaking when the announcement was made, I completely understand and support the rationale behind it.

It would be much worse if the solution is half-baked and released to the world. The damage it causes could be devastating.

TypeScript 4.7 is trying to provide the support again, and hopefully it's here to stay.

The big migration

So how to migrate your code to ESM?

Upgrade TypeScript:

npm install --dev typescript@rc

yarn add -D typescript@rc

(or typescript without @rc when 4.7 is released).

Update your package.json to:

{
"type": "module",
"exports": {
".": {
"import": {
"default": "<path to main>",
"types": "<path to d.ts>"
}
}
}
}

In TypeScript 4.7 announcement and other places mentioned you can provide fallback for CommonJS, but in general it could cause problems as that will increase the chance of having multiple copies of your library exist in runtime.

Update your import statements to add .js extension. e.g.:

// import { foo } from './foo'
import { foo } from './foo.js'

Update your project files from .js to either .cjs or .mjs.

When you set the package to use "type": "module", Your project files such as jest.config.js and babel.config.js becomes ambiguous, and you have to update the file extension to reflect if the file is a CommonJS or ESM file.

Some tools might not work with the new module format. You have to disable them for now.

To me, I found that size-limit does not support it yet.

Also, I found that I can't import the default export from TypeScript package transpiled to CommonJS.

Don't know if that is by design or it is a bug yet.

You can take a look at the global-store PR as an example.

Happy Coding, 🧑‍💻