Configuring TypeScript in Node.js to Emit Native ESM Code
ECMAScript modules (ESM) support is now considered stable in Node.js and can be enabled by setting type
to module
in package.json
. In this tutorial, we are going to configure the TypeScript compiler to emit JavaScript code with ESM import
and export
statements, instead of the traditional CommonJS require()
and module.exports
.
We will be using Node.js v20.
node -v
v20.7.0
Setup TypeScript compiler
Start by installing the latest TypeScript compiler.
npm install typescript --save-dev
npx tsc --version
# Version 5.2.2
In your project’s root directory, create a new configuration file tsconfig.json
for the TypeScript compiler:
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"outDir": "dist",
},
"include": ["src"]
}
target
: The ECMAScript version to use when transpiling TypeScript. Node.js v20 supports ES2022 features.module
: The module system to use.nodenext
mode enables integration with Node’s ESM.outDir
: The output directory for JavaScript code.include
: The directory containing our TypeScript source.
We should also set type
to module
in our package.json
to enable ESM support in Node.js.
{
...
"type": "module",
...
}
Create an app
Let’s create a simple application to test the TypeScript compiler.
This is the directory structure of our project.
.
└─dist
└─src
│ ├─app.ts
│ └─MyClass.ts
└─package.json
└─tsconfig.json
In the src
directory, create a file MyClass.ts
:
export class MyClass {
name: string;
constructor(name: string) {
this.name = name;
}
getMessage(): string {
return `Hello ${this.name}`;
}
}
In the same directory also create app.ts
:
import { MyClass } from "./MyClass.js";
const myInstance = new MyClass("world");
console.log(myInstance.getMessage());
Notice that when importing MyClass
in our TypeScript code we used the .js
extension, despite that there is currently no MyClass.js
in the filesystem, and our actual source file is named MyClass.ts
. Due to the way TypeScript is currently implemented, we have to make sure that all import paths in our code point to files that will exist after TS is transpiled to JavaScript.
That is not an issue when working with CommonJS modules, since we typically import paths without adding extensions, but that approach is not compatible with ESM, as can be seen below.
For those thinking that it would make more sense to point to MyClass.ts
in our TypeScript code and it should be the compiler’s responsibility to rename the path to MyClass.js
during the compilation step - well, you’re right. You should know that the current approach was the result of an extremely unpopular decision made by Microsoft developers.
Build & Run app
Running npx tsc
will transpile TypeScript to JavaScript. Let’s put that command into package.json
.
...
"scripts": {
"start": "node dist/app.js",
"build": "npx tsc"
},
...
Run the build script.
npm run build
In the dist
directory, there should now be a app.js
and a MyClass.js
. Open app.js
to confirm that the TypeScript compiler has emitted ESM code.
import { MyClass } from "./MyClass.js";
const myInstance = new MyClass("world");
console.log(myInstance.getMessage());
Finally, run our app.
npm start
# Hello world