At times, the TypeScript compiler options can be confusing. There are just so many of them and they are not always
the end? Let's first have a look at what the docs have to say about the
How the TypeScript docs describe the module compiler option
So what the docs are specifying for this compiler option is currently the following:
Specify module code generation: "None", "CommonJS", "AMD", "System", "UMD", "ES6", "ES2015" or "ESNext". ► Only "AMD" and "System" can be used in conjunction with --outFile. ► "ES6" and "ES2015" values may be used when targeting "ES5" or lower.
with the beautiful default
target === "ES3" or "ES5" ? "CommonJS" : "ES6".
An introduction to modules
To understand what the
modules compiler flag is doing, we first have to understand
could still work together, was that different pieces of code bound to the global context, and then other pieces of
code could use it. So for example, you'd import
jquery in a
tag at the top of your
head tag in HTML, and then a subsequent script could use it
by accessing the globally introduced
$ variable. Of course this has several
drawbacks, for example that the order of the script tags matters! Another important problem is that it's not
modular at all, so if you imported a library through a script tag, you imported all of its parts, not just the
ones you needed. You were also lacking the means of organizing your own code into multiple files without
cluttering up the global namespace. Pretty crazy huh?
To make it short, we'll not be going into
System, because those are less common use cases. Here's a comparison
ES6 / ES2015 / ESNext:
|CommonJS||ES6 / ES2015 / ESNext|
|Understood by||node.js||Modern Browsers, node.js|
require syntax in CommonJS. You might not even have known up to this point, that the
require is actually just CommonJS' way to import a module.
For those of you working more with TypeScript, the right side will look more familiar. But as you also know, what
you write is not what you get, because your code will be transpiled
So if you choose
adheres to the CommonJS syntax:
You can try this yourself on the TypeScript playground by selecting "CommonJS" as the module, as is illustrated here.
Now on the other hand, if you choose
module: "ESNext" or ES6 or ES2015
As you can notice, the code didn't change at all, except that
const got changed to
var since we chose
ES5 as a compile target.
Now you must be asking yourself: "Ok, that's nice and all, we have different module definitions, but when should I use which one?!". This question is what we'll answer next.
CommonJS vs ESNext
Generally speaking, ESNext is the way forward. With a big BUT.
ECMA came a bit late to the party, that's why other module systems arose, but now that they've defined an official standard for modules, all systems are trying to move in this direction. While all modern browsers now support ES Modules, node.js also adopted support for them.
But since node.js already had a module system in place with CommonJS, it still feels a bit weird, because ES
Modules need to live in files with the extension
.mjs. TypeScript also isn't a great
help, since you cannot choose the extension of the transpiled files, they're all
And changing this extension with some script also doesn't solve your problem, because the files are referenced
extensionless by other files. So all in all this can be summarized as: Using TypeScript, node.js and the
TypeScript compiler option
"module": "esnext" together is next to impossible. So if
you're building code that should be ran with node.js, in 2020, you should still choose
If you're building code that is to be used by browsers, things are different. Browsers don't have the slightest clue about CommonJs modules, but all modern browsers on the other hand now do support ES modules. If you want to target older browsers (looking at you, Internet Explorer), you will probably want to bundle your code together, which can be done for example by Webpack.