TypeScript Module System in Browser and Server


typescript

TypeScript Module System in Browser and Server


Daniel Shin - August 15, 2015

This post is for self-documenting purpose.

In a nutshell, there are three module systems provided by TypeScript.

Internal Module

Internal Module is mostly used in browser setting. It is a simply syntactic-sugary wrapper for namespacing-object pattern in vanilla javascript with added benefits of types. That’s it.

/** wrapper.ts **/
module Wrapper {
  export function hello() {}
}

/** importer.ts **/
// <reference path="./wrapper.js">
Wrapper.hello()

more or less, compiles to

/** wrapper.js **/
var Wrapper = (function() {
  function hello() {}
})()

/** importer.js **/
Wrapper.hello()

I’m simplifying this immensely but the essence of it is pretty straight-forward. Internal module enables splitting a single module across multiple files (purely for an organization purpose) as long as they all files share the same module.

Internal module is a superset of what vanilla javascript accomplishes using objects. Javascript in browser has only one namespace; window object. So by default, whatever you declare without a wrapper becomes global, and this enables javascript files to be concatenated into one big file.

Being able to output one file is the benefit of using internal module versus external module and is especially useful in browser setting where every bit of latency for individual network requests matters.

tsc --output FILE ... (outputs to a single file) works only with internal module for the above reason as well.

External Module (AMD)

TypeScript also has the concept of an external module. External modules are used in two cases: node.js and require.js.
Applications not using node.js or require.js do not need to use external modules and can be best be organized using the internal module concept outlined above.
In external modules, relationship between files are specified in terms of imports and exports in the file level.
In TypeScript, any file containing a top-level import or export is considered an external module.

TypeScript Handbook

AMD stands for Asynchronous Module Definition. It is a Javascript specification for loading modules asynchronously. One of its most popular implementations is requirejs.

requirejs was created to bring module system into Javascript in browser.

Enabling tsc --module amd means that you must depend on requirejs, otherwise hell breaks loose.

/** wrapper.ts **/
export function hello() {}

/** importer.ts **/
import {Wrapper} from "./wrapper"
Wrapper.hello()

is an equivalent of the code from internal module.

Notably, you do not need to wrap hello() function in module {}, since every file is, in fact, a single module. And this is the reason why concatenating javascript files using external module isn’t possible. Loading modules explicitly rely on the physical location of the file itself.

Just as there is a one-to-one correspondence between JS files and modules, TypeScript has a one-to-one correspondence between external module source files and their emitted JS files.
One effect of this is that it’s not possible to use the –out compiler switch to concatenate mulitple external module source files into a single JavaScript file

TypeScript Handbook#Trade-offs for External Modules

However, by sacrificing the ability to output a single file, it gains a few benefits.

Most importantly, you can namespace for each file. This eliminates a risk for polluting global namespace.

You no more need to specify /// <reference path=""> or module {}. Simply do import and export.

Also you get to write same code for both browser and server (this is further discussed in next).

External Module (commonjs)

CommonJS is the module system adopted in Node.JS.

As far as .ts file goes, there is no noticeable difference between amd and commonjs. tsc TypeScript compiler simply outputs commonjs compliant javascript file instead of the amd compliant. That’s it.

This is often why using external module is more beneficial to internal module despite not being able to concat. You get zero cognitive overhead in switching different module systems between the browser(well, the absence of it) and the server and enables true code sharing without relying on a library like browserify.

While I keep saying that outputting to one file is not possible with AMD, I’m almost certain that there is a library that does that for requirejs considering its popularity, but I just haven’t looked into it yet.

As for commonjs, there is no reason to output a single file, since you already own the source files. Not having to download it from external source eliminates an issue regarding latency.


Overall, this post was just me rambling some of my unorganized thoughts on module systems. I’ve only used TS extensively in server and now that I use it in browser, I’ve encountered many subtle gotchas that needed to be documented; thus this rambling post.

Addendum

As of TypeScript 1.5 with ES6, there is yet another module system; ES6 module system.

ES6 module system is supported natively by JavaScript engine and it is very good. However, it is only available in ES6.

As ES6 is supported in more and more browsers and adopted by people, ES6 module system will unify both browser and server module system into one.

But that’ll take a very long time looking back in the history. 3-4 years minimum. Likely about 5 years.

So yeah, just keep using TypeScript module system.

To target ES6, we can do,
tsc --target es6 ...
instead of
tsc --module $MODULE --target es5 ...

Note the absence of --module flag. Module is natively supported.