Building heterogeneous TypeScript libraries

Tue Nov 20, 2012

A technique for compiling one or more TypeScript source files to a single JavaScript library file that can be used in both the browser and by Node.js applications.

By way of example

Our library source consists of a single TypeScript module called Lib spread across multiple source files (lib1.ts and lib2.ts) that exports a public API:

lib1.ts

module Lib {
  export function f() {}
}

declare var exports: any;
if (typeof exports != 'undefined') {
  exports.f = Lib.f;
}

lib2.ts

module Lib {
  export var v = {foo: 42};
}

declare var exports: any;
if (typeof exports != 'undefined') {
  exports.v = Lib.v;
}

The source file are compiled to a single JavaScript library lib.js using the TypeScript compiler:

tsc --out lib.js lib1.ts lib2.ts

lib.js

var Lib;
(function (Lib) {
    function f() {
    }
    Lib.f = f;
})(Lib || (Lib = {}));
if(typeof exports != 'undefined') {
    exports.f = Lib.f;
}
var Lib;
(function (Lib) {
    Lib.v = {
        foo: 42
    };
})(Lib || (Lib = {}));
if(typeof exports != 'undefined') {
    exports.v = Lib.v;
}

lib.js file now can be included on an HTML page with:

<script type="text/javascript" src="lib.js"></script>

Or in a Node.js application with:

var Lib = require('./lib.js');

The APIs are accessed via the module name e.g. Lib.f(), Lib.v.

Explanatory notes

The key to being able to import the code into Node.js with require('./lib.js') is conditionally assigning public API objects to properties of the global exports object e.g.

declare var exports: any;
if (typeof exports != 'undefined') {
  exports.f = Lib.f;
}
  • exports is a global object defined by CommonJS compatible loaders such as Node’s.
  • This code must be placed at the end of the source file outside the module declaration.
  • exports will not be assigned in the browser (or any non-CommonJS environment where exports is not defined).
  • To minimize browser global namespace pollution all source is enveloped in a single open module (Lib) – this is not necessary in a module loader environment (e.g. Node.js).
  • Multi-file “open” modules are not truly open – you must export any module objects that need to be accessed across file boundaries.
  • TypeScript can emit CommonJS modules directly by prefixing module with the export keyword and using the compiler --module commonjs option, but there are two problems with this approach:
    1. The generated code can only be loaded with a module loader and cannot be used in a browser unless you also depoly a suitable browser module loader.
    2. External modules must reside in a single source file (they are not open).

Scoping function wrapper

A variation of the above technique is to wrap the combined compiled file with a scoping function. This will ensure non-exported top level objects do not pollute the browser global namespace:

(function() {
  :
})();

The browser API is hoisted to the global namespace by assignment to the browser window object. The previous example becomes:

declare var exports: any;
if (typeof exports != 'undefined') {
  exports.f = Lib.f;
}
else if (typeof window !== 'undefined') {
  window['f'] = Lib.f;
}

References



  « Previous: Next: »