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:

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
exports.hello = 'Hello!';
function sayHello() {
    console.log(exports.hello);
}
exports.sayHello = sayHello;
export const hello = 'Hello!';
export function sayHello() {
    console.log(hello);
}
Import
const hello = require('./hello.js');
console.log(hello.hello);
hello.sayHello();
import {hello, sayHello} from './hello.js'
console.log(hello);
sayHello();
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.

Interested in web development?


Learn to Write Clean Code

Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin)