Using ECMAScript modules for a React application using TypeScript and SWC

Patrick Lafrance
Workleap
Published in
4 min readMar 24, 2023

--

Photo by La-Rel Easter on Unsplash

What is ESM?

ECMAScript modules (ESM) is the official JavaScript module system to write isolated and modularized pieces of code.

It’s the latest and, hopefully, final iteration of a long series of module systems that have emerged in the frontend space since the 2000s, including CommonJS, AMD, and RequireJS, to name a few.

With ESM, applications can now import and exports modules with the following syntax:

// ESM

export function fct() { ... }

...

import { fct } from "./my-fct.js";

Before the introduction of ESM, the popular way to import and exports modules was with CommonJS (the NodeJS way):

// CommonJS

module.exports = function fct() { ... }

...

const fct = require("./my-fct");

The main difference with ESM is that the import and export statements are now static. We'll explain in the next section why it matters.

Benefits of ESM

Standardized specification

The most important benefit of ESM is that it is a standardized specification. By adopting ESM, your code will be future-proof because it will be compatible with any existing and future platforms that run JS code, including Web browsers, NodeJS and alternate runtimes like Deno.

Tree-shaking

ESM also enables bundlers to do “dead code elimination,” which is commonly referred to as “tree-shaking.”

Since ESM import and export statements are static, bundlers can now remove unused code during the build process, resulting in smaller bundles. This was not possible with older module systems like CommonJS and AMD because import statements were resolved at runtime.

Faster development build

Given that ESM is compatible with modern Web browsers, a transpilation step may not be necessary anymore to run the code in a browser during development, depending on the technology stack of your application.

Omitting the transpilation step results in a faster feedback loop during development.

Using ESM for a React and TypeScript application

To start using ESM, there’s a few coding habits to change and the project tooling configurations must be updated to target an ESM environment.

Prerequisites

  • Install at least the latest LTS version of Node.js
  • Install TypeScript version 5+ (it also work with previous releases if your bundler can play well with TypeScript only accepting import paths having a “.js” file extension)

Update the project configurations

For a Web application project, tsc will not emit any type declaration files as it’s only used to “lint” the codebase. Transpiling TypeScript code to native JavaScript is the bundler responsibility.

The following steps take for granted that you are using a bundler and that the bundler is supporting ESM module resolution system.

You can find a proof of concept on Github using those configurations with Webpack 5 as the bundler.

1- Define the project as an ESM project:

// package.json

{
"type": "module"
}

2- Update TypeScript configuration:

// tsconfig.json

{
// Don't emit TS declaration files.
noEmit: true,
// The input module resolution system, "bundler" mode support an hybrid of CommonJS and ESM.
// If it's not a requirement for your project, you can use "nodenext" instead.
"moduleResolution": "bundler",
// Allow using ".ts" and ".tsx" file extensions in import paths.
"allowImportingTsExtensions": true
}

3- Update SWC configuration:

// .swcrc

{
"jsc": {
// The output environment that the code will be compiled for.
// You can change this if you want to emit code for a different environment.
"target": "esnext"
},
"module": {
// The output module resolution system that the code will be compiled for.
// You can change this if you want to emit code target another module resolution system.
"type": "es6"
}
}

4- Update Jest configuration:

// jest.config.js

{
transform: {
"^.+\\.(t|j)sx?$": [
"@swc/jest",
{
// Your SWC config as there seems to be an issue with @swc/jest
// to pick up the .swcrc config file.
// https://github.com/swc-project/swc-node/issues/635#issuecomment-1070766669
}
]
},
extensionsToTreatAsEsm: [".ts", ".tsx"]
}

5- Update ESLint configuration:

// .eslintrc.json

{
"env": {
"es6": true,
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}

Change coding habits

Now that the project is configured properly. You can start using ESM in your code.

1- Use import and export statements rather than require and module.exports:

// ESM

export function fct() { ... }

...

import { fct } from "./my-fct.js";

2- Add file extension to import paths:

// Stop doing
import { fct } from "./my-fct";

// Start doing
import { fct } from "./my-fct.js";

3- Do not rely on inferrence to deduce the index.js file path:

// Stop doing
import { fct } from "./module";

// Start doing
import { fct } from "./module/index.js";

// Or
import { fct } from "./module/my-fct.js";

4- When importing TypeScript files, keep their original .ts or .tsx file extension in the import paths:

// Import a function from a .ts file
import { fct } from "./my-fct.ts";

// Import a component from .tsx file
import { Button } from "./my-button-component.tsx";

And that’s it, your Web application project should now benefit from ESM!

--

--