Table of Contents
NodeJs ESM support is finally here

The TypeScript 4.7
version is planned to be released on the 24th of May. After a not-so-exciting TypeScript 4.6 release, we are in for a treat. It was meant to be hiring out some TypeScript behavior like discriminator.
It was well known that the TypeScript team was putting a hard focus on the NodeJs integration. Its support for ES Modules
is a long-awaited feature. It was released on the 4.5
version under an experimental flag. Why? Many issues needed to be polished, and it was an excellent way to start getting feedback and issues.
Finally, the NodeJs integration is shipped as a stable feature. But that is not the only feature I am excited about. This 4.7
release is fantastic if you are a React Native developer, thanks to its moduleSuffixes
feature.
In this article, I will highlight the most relevant features. As always, you can always play around with the new version here.
The module foundation of Node.js has been CommonJS. As there is a shift toward polymorphic apps, the urge to support ECMAScript modules has grown. For the past years, Node.js has been working to support those. From the Node 12 version onwards, the support for the ES Modules is widely available.
There are now two new module
config settings: nodenext
and node12
.
"compilerOptions":
"module": "nodenext"
The “type” property in package.json
Through the package.json
file, we can now configure the module format that Node will use for the files.
...
"name": "my package",
"type": "module",
...
What are the options? We can choose between module
for ES Modules
or commonjs
for traditional CommonJS modules.
This behavior will be embraced for all regular JavaScript files with no extension. TypeScript will be embracing that same system behavior with its .ts
files.
How does it work in practice? When the compiler finds a .ts/.tsx/.js/.jsx
it will look at its nearest package.json
file to determine its module flavor.
Configuration through files extensions
We have seen the default behavior. How can a single file determine its own module system? By just changing its file extension.
Let’s see the new conventions:
.cjs/.cjx
: the file will be imported in CommonJS format regardless of the nearest parent"type"
configuration..cts/.ctx/.d.cts
: the files will be imported in CommonJS format regardless of the parent"type"
specification. When emitting files, it will output its corresponding.
Let’s see what extension we can now use:mjs
,.mjx
or declarations with the.d.cts
extension..mjs/.mts/.mtx/d.mts
: the files will be imported in ECMAScript Modules format regardless of the parent"type"
specification. When emitting files, it will output its corresponding.mjs
,.mjx
or declarations with the.d.mts
extension.
Interoperability
Given that now you can import different module types by tuning its file system, they both need to work together. The ES Modules are easy as it’s just a matter of transpilation. For ES Modules to import CommonJS, it will treat them as they had default export.
Here’s an example:
The “exports” property in package.json
We have seen previously how a type
was added in the package.json
file to configure the module default. There is equally a new property to export the packages’ exports
.
With that property, we fine-tune how we expose our files. Here’s the code:
In the above, we can see:
- how we defined specific imports for
ESM
orCommonJS
- how we can define TypeScript specific definitions within those ones
- how we can define fallbacks for older versions
Takeaways from ES Modules integration
What features do ES Modules bring? What should we pay attention to? Let’s see a summary list:
- Feature: using the
import/export
statement syntax - Feature: top-level await available with
nodenext
- Interop: Some global keywords like
require
will not work on ES Modules
There is always a bit of ambiguity in the JS ecosystem where a file is a module or just a script. For that reason, a new compiler option was introduced: moduleDetection.moduleDetection
. It lets us configure how modules are parsed.
It may take one of those three values:
1. auto
This is the new default from 4.7
. TypeScript will look for imports
and exports
statements but will also check:
- the
type
filed inpackage.json
- if the file is running under
--jsx react-jsx
2. legacy
This value has the same behavior as previous TS versions. It looks into import
and exports
statements to check if files are modules or not.
3. force
This mode will force every file to be treated as a module. All non-declaration files will be treated independently as to how module
, moduleResolution
and jsx
are configured.
When targeting only browsers, it is preferable to have moduleDetection: force
and module: esnext
. We don’t want commonJs
modules in that scenario.
With React-Native polymorphic applications, we have the choice to configure which file will be loaded in each platform by changing its extension.
index.native.ts
: will be loaded in RN bundlerindex.ts
: will be used by web bundler
However, there was a caveat when using TypeScript. The TypeScript compiler wouldn’t know about the existence of .native.ts
. This will now be possible by using the moduleSuffixes
compile option.
"compilerOptions":
"moduleSuffixes": [".ios", ".native", ""]
Note that ""
is required to look at the default ts
files.
All the React-Native
developers will surely be cheering about this new feature and rushing to upgrade.
Sometimes we want to do simple things like creating a more specific function from a generic one. That is possible to do currently with a function wrapper. However, in this new 4.7
version, we can easily create an alias from generic functions. Here’s an example:
You can now do cool type aliasing like the following:
It is a simple but neat functionality that will open the door to many new types of patterns.
A new improvement on the computed properties has been included in this release. TypeScript will now consider type guards in wider scenarios.
Here’s an example:
Another great thing is that -strictPropertyInitialization
will be able to include those scenario checks.
In summary, this is a great release. The support for ESM
modules in Node
has been a big focus of the team, and it’s great to see it live. That does not mean there are no more things to like about this release.
There are also some nice features like groups-aware
organize imports, or object method snippet completions.
Cheers!