The TypeScript Module Compiler Option

April 2020

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:

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:

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.