There is lots of resources about this, yet it is easily confusing and I can't find any recent resources about this:
Today in 2024, I want to create a library, written in tyescript. It will be used in React projects that use a webpack bundler. And the app will be rendered on the server (node.js) and in the browser.
how do I configure tsconfig.json? Which setting do I choose for module
?
Context:
The library already exists in CJS, but in a current big rewrite I am considering to upgrade to ESM, since we have issues in our apps that use this library (which are also CJS at the moment) with importing ESM only modules. And also, with CJS we can't make use of automatically split bundles when using dynamic imports in webpack.
The documentation
The typescript reference for module
states that es2015
,es2020
,es2022
,esnext
should not be used for node.js
And it states that
node16 and nodenext are the only correct module options for all apps and libraries that are intended to run in Node.js v12 or later
But does not mention if nodenext
or node16
can be used for browsers / bundlers like webpack as well.
What is it? Which should I chose?
I wonder if should I go for a split route of publishing a /lib/cjs
and /lib/esm
version, with require
and module
fields in the export
settings of package.json
?
That still leaves me with the question which module
output setting to use in tsconfig so that both browser (through webpack) and node.js can use ESM. Or should I opt for one of these environments to keep using CJS?
Due to historical reasons, there are just too many combinations, but most are not required anymore. In summary, the following four rules are more than enough if you want to publish ESM-only package that can be used by both Node.js and browser:
"type": "module'
."module": "NodeNext"
"moduleResolution": "NodeNext"
"module": "ESNext"
"moduleResolution": "Bundler"
"module": "NodeNext"
"moduleResolution": "NodeNext"
The combination of NodeNext + NodeNext
is a subset of ESNext + Bundler
. The alternative way of saying this is that NodeNext + NodeNext
is ESM with constraints as set up by Node.js module resolution algorithm. The most common example being the mandatory requirement of having file extension in the import statements e.g. import {} from 'other.js'
in NodeNext + NodeNext
. Such restriction does not apply in ESNext + Bundler
mode.
So, if you are publishing for both Node.js (expecting to use without bundling) and for frontend (expecting to use import-maps
or bundler), then you should use module: NodeNext + moduleResolution: NodeNext
.
This ensures that TypeScript (and, other modern bundlers) respects main
, exports
declarations as specified by package.json
specification.
Also, keep in mind that "type": "module"
is important. In its absence, even with module: NodeNext
, typescript compiler will compile *.ts
files to *.js
with CJS require
statements and *.mts
files to *.mjs
with ESM import statements.
Finally, there is alternative way of thinking about it too! If you are using TypeScript as a compiler to compile your library, then you should use NodeNext + NodeNext
. And, if you are using bundler, then you have a choice of either combination (although I still recommend using NodeNext + NodeNext
due to strict semantics it introduces).
For more detailed, in-depth explanation of module
and moduleResolution
, this is a good read.