Skip to main content

Fixing TypeScript code for ESM with `tsc-esm-fix`

ยท 3 min read
Homa Wong (unional)

TypeScript 4.7 is released ๐ŸŽ‰!

The wait is finally over. You are excited to start your migration.

So you update your package.json, update your tsconfig.json, and voila, you are greeted with a boat load of errors.

What you need to do is to update your import statements:

  • add .js extension for files, or
  • add /index.js for folders.

You can do that manually, or you can use a tool to help you on that.

The tool (tsc-esm-fix) is the focus of the topic today. But before that, let me highlight a few things about the current state of the type: module support in TypeScript 4.7, so that you can decide should you take the jump right now, or should you wait a little bit longer.

There are four issues that I'm aware of related to type: module.

The .js extension debateโ€‹

The first one is a debate around the .js extension.

The takeaways are:

  • .js is artificial, pointing to a non-existing file
  • TypeScript team do not want to rewrite JS code
  • NodeJS needs extra tricks to detect the source type

You can read about the detail here:

broken source mapโ€‹

The second one is that source map is currently broken.

If your code will be used in the browser, you need to weight that in.

You can read about the detail here: https://github.com/microsoft/TypeScript/issues/49335

ts-loader workaroundโ€‹

The third one is that currently ts-loader does not support ESM module out of the box.

You have to do two things.

  1. use the NormalModuleReplacementPlugin:
{
plugins: [
new NormalModuleReplacementPlugin(/.js$/, (resource) => {
if (/node_modules/.test(resource.context)) return
resource.request = resource.request.replace(/.js$/, '')
})
]
}
  1. Do not use transpileOnly: true. It does not work.

If I have time, I might dig in to help ts-loader about this, but not sure when I'll be able to do that.

You can read about the detail here: https://github.com/TypeStrong/ts-loader/issues/1463

jest ESM workaroundโ€‹

The last one is an outstanding one, that jest does not have native support of ESM.

Meaning when you use ts-jest, you also need to use babel-jest to transpile dependencies within node_modules.

Here is a nutshell of what you need to do:

// jest.config.mjs
export default {
preset: 'ts-jest/presets/default-esm',
globals: {
'ts-jest': {
useESM: true
}
},
moduleNameMapper: {
// remove the phantom `.js` extension
'^(\\.{1,2}/.*)\\.js$': '$1',
// If dependency doing `import ... from '#<pkg>'.
// e.g. `chalk` has this: `import ... form '#ansi-styles'`
'#(.*)': '<rootDir>/node_modules/$1'
},
transformIgnorePatterns: [
// Need to MANUALLY identify each ESM package, one by one
'node_modules/(?!(@unional\\fixture|chalk)/)'
],
transform: {
'^.+\\.(js|jsx|mjs)$': 'babel-jest',
}
}

You can find related information here: https://dev.to/steveruizok/jest-and-esm-cannot-use-import-statement-outside-a-module-4mmj

tsc-esm-fixโ€‹

Now, if you decide to move ahead, then you can use tsc-esm-fix to help you.

I used it to help me migrating type-plus, which has 140+ files. I'm using it for other packages as I'm writing this blog.

I have discovered a few bugs and @antongolub is very quick to fix them. Go to the repo and give it a star โญ!

The easiest way to use it is through npx or yarn dlx:

npx tsc-esm-fix --src=<src> --ext='.js'

# or
yarn dlx tsc-esm-fix --src=<src> --ext='.js'

For type-plus, since I put the source code under the ts folder, so the command is:

npx tsc-esm-fix --src=ts --ext='.js'

If you put your source code under the typical src folder, then the command is:

npx tsc-esm-fix --src=src --ext='.js'

Happy Coding, ๐Ÿง‘โ€๐Ÿ’ป