As part of the TypeScript 4.7 release comes a major upgrade to ECMAScript Module Support for Node.js. This post takes a look at what that means.
A short history of ECMAScript modules
Whilst writing code using ECMAScript module semantics came quickly for front end, for the back end (which is generally Node.js) that has not the case. There’s a number of reasons for this:
- There was already an established module system used in Node.js called CommonJS
- Node.js itself did not initially offer support for ECMAScript modules; in large part because of the problems associated with being able to support CommonJS as well as ECMAScript modules
However, with the release Node.js 14 support for ECMAScript modules (also known as “ESM”) landed. If you’re interested in the details of that module support then it’s worth reading this post on ECMAScript modules.
The TypeScript team have been experimenting with ways to offer support for ECMAScript modules from a Node.js perspective, and with TypeScript 4.7 support is being released.
In this post we’ll test drive that support by attempting to build a simple module in TypeScript using the new ECMAScript modules support. As we do this, we’ll discuss what it looks like to author ECMAScript modules for Node.js in TypeScript.
Making a module
We’re going to make a module named
greeter — let’s initialize it:
mkdir greeter cd greeter npm init --yes
We now have a
package.json that looks something like this:
"name": "greeter", "version": "1.0.0", "description": "", "main": "index.js", "scripts": "test": "echo "Error: no test specified" && exit 1" , "keywords": , "author": "", "license": "ISC"
Node.js supports a new setting in
type. This can be set to either “module” or “commonjs”. To quote the docs:
Files ending with
.jsare loaded as ES modules when the nearest parent package.json file contains a top-level field
"type"with a value of
With that in mind, we’ll add a
"type": "module" to our
We’re now ECMAScript module support compliant, let’s start adding some TypeScript.
Adding TypeScript 4.7
In order that we can make use of TypeScript ECMAScript modules support we’re going to install TypeScript 4.7 (currently in beta):
npm install [email protected] --save
With this in place, we’ll initialize a TypeScript project:
npx tsc --init
This will create a
tsconfig.json file which contains many options. We will tweak the
module option to be
nodenext to opt into ECMAScript module support:
We’ve also set the
declaration option such that
.d.ts files will be generated. We’ll also update the
"scripts" section of our
package.json to include
"scripts": "build": "tsc", "start": "node lib/index.js" ,
Writing TypeScript ECMAScript modules
With all that set up, we’re ready to write some TypeScript ECMAScript modules. First we’ll write a
export function helloWorld(): string return 'hello world!';
There is nothing new or surprising about this; it’s just a module exporting a single function named
helloWorld. It becomes more interesting as we write our
import helloWorld from './greetings.js'; const greeting = helloWorld(); console.log(greeting);
The code above imports our
helloWorld function and then executes it; writing the output to the console.
Not particularly noteworthy; however, the way we import is.
We are importing from
'./greetings.js'. In the past we would have written:
import helloWorld from './greetings';
Now we write:
import helloWorld from './greetings.js';
This can feel slightly odd and unnatural because we have no
greetings.js in our codebase; only
The easiest way to demonstrate that this is legitimate is to run the following code:
npm run build && npm start
Which results in:
> [email protected] build > tsc > [email protected] start > node lib/index.js hello world!
So, it works!
ECMAScript and CommonJS side by side
Part of ECMAScript module support is the ability to specify the module type of a file based on the file suffix. If you use
.mjs, you’re explicitly saying a file is an ECMAScript module. If you use
.cjs, you’re explicitly saying a file is an CommonJS module. If you’re authoring with TypeScript, you’d use
cts respectively and they’d be transpiled to
Happily, Node.js allows ES modules to import CommonJS modules as if they were ES modules with a default export; which is good news for interop. Let’s test that out by writing a
export function helloOldWorld(): string return 'hello old world!';
Exactly the same syntax as before. We’ll adjust our
index.ts to consume this:
import helloWorld from './greetings.js'; import helloOldWorld from './oldGreetings.cjs'; console.log(helloWorld()); console.log(helloOldWorld());
Note that we’re importing from
We’ll see if it works:
npm run build && npm start
Which results in:
> [email protected] build > tsc > [email protected] start > node lib/index.js hello world! hello old world!
It does work!
What files are emitted?
Before we close out, it might be interesting to look at what TypeScript is doing when we run our
greetings.ts file has resulted in
greetings.js and a
greetings.d.ts files, whereas
oldGreetings.cts has resulted in
oldGreetings.cjs and a
oldGreetings.d.cts files; reflecting the different module types represented.
export function helloWorld() return 'hello world!';
This is the same code as
greetings.ts but with types stripped. However, if we look at
oldGreetings.cjs, we see this:
'use strict'; Object.defineProperty(exports, '__esModule', value: true ); exports.helloOldWorld = void 0; function helloOldWorld() return 'hello old world!'; exports.helloOldWorld = helloOldWorld;
In the middle is the same code as
oldGreetings.cts, but with types stripped, but around that boilerplate code that TypeScript is emitting for us to aid in interop.
We’ve seen what TypeScript support for ECMAScript modules looks like, and how to set up a module to embrace it.
If you’d like to read up further on the topic, the TypeScript 4.7 beta release notes are an excellent resource.