At times, the TypeScript compiler options can be confusing. There are just so many of them and they are not always
that clear to someone who didn't go too deep into the rabbit hole of the JavaScript universe. But this is
basically what it comes down to: How do you want your generated JavaScript to look like in
the end? Let's first have a look at what the docs have to say about the "module"
compiler option.
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"
.
To understand which option you should choose mostly comes down to understanding JavaScript modules in general and knowing which system (node.js, browser) understands which module syntax. So let's continue with this, generating an understanding of JavaScript modules.
An introduction to modules
To understand what the modules
compiler flag is doing, we first have to understand
what a module is. As crazy as it may sound, in JavaScript, there wasn't always a way to write modular code, in the
sense that one JavaScript file could just import functionality of another one. The way different pieces of code
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 script
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?
With the introduction of node.js, but also with the rise of Single Page Applications with more JavaScript logic, those impediments were a real problem. But since there wasn't one official way (defined by the ECMA standards body) how to write modules, multiple different attempts to bring modules to the JavaScript world emerged. This makes the whole thing quite a bit more complicated, as you get different module syntax', each only being supported by some JavaScript engines. Some aren't even supported by any JavaScript engine directly, but they need to be converted into a non-modular form first by a bundler!
To make it short, we'll not be going into AMD
, UMD
or
System
, because those are less common use cases. Here's a comparison
between CommonJS
and ES6 / ES2015 / ESNext
:
CommonJS | ES6 / ES2015 / ESNext | |
---|---|---|
Export |
|
|
Import |
|
|
Understood by | node.js | Modern Browsers, node.js |
Website | http://www.commonjs.org/ | https://www.ecma-international.org |
Now those of you somewhat familiar with node.js applications in JavaScript will immediately recognise the
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
by tsc
into JavaScript code before it's actually used by a JavaScript engine.
So if you choose module: "CommonJS"
, the JavaScript code that will be generated
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
you
will get:
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 .js
.
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 "module":
"commonjs"
.
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.
Conclusion
Understanding modules in JavaScript is difficult, which is what makes understanding the TypeScript compiler option corresponding to JavaScript modules difficult. By knowing where you want to run your code, you can make a decision whether to choose CommonJS in the case of node.js or esnext if your target are browsers.