TypeScript: exposing module types in the global context and why to avoid it

Javascript modules have been around for a while, and there are plenty of good reasons why to use them, instead of writing code directly in the global scope.

However, you might find yourself working on a legacy piece of code that was written without taking modules into account and considering what to do to start refactoring it into a module based pattern, like AMD or CommonJS. Well, that was my case.

What I wanted to do was to take a big, monolithic library and break it incrementally into smaller modules. So the initial idea was to take a few functions, wrap them in a module, load the module back into the global scope and expose the module functions back through the library as to avoid breaking changes for its consumers.

This is how you could do that, keeping maintaining the type declarations in TypeScript.

Assume the original library, MyLib.ts, looks like this:

// MyLib.ts
namespace MyLib  
    export class MyClass {
           public getWork(): string {
                  return "work";

    export class Test {
           public doLibWork(): void {
                  var x: MyClass = new MyClass();

And we want to move MyClass out to a module but still export MyLib.MyClass so any consumers are not affected by the change.

This is how the new module would look like, let's call it moduleA:

// moduleA.ts
export default class MyClass {  
       public getWork(): string {
              return "work";

Back in MyLib.ts, if we don't want to make it a module, we can't really import any modules in that file using Typescript's top level import statement. That's because any files with top level import or export statements are considered modules by Typescript. So we create a separate file to handle the import, let's call it MyLib.imports.ts:

// MyLib.imports.ts
import MyClassFromModule from 'moduleA'

declare global {  
    namespace MyLib {
       export type MyClass = MyClassFromModule;
       export const MyClass: typeof MyClassFromModule;

(<any>window).MyLib = { MyClass: MyClassFromModule };

Let's break it down.

  1. import MyClassFromModule from 'moduleA' loads moduleA module, this is, the module that contains MyClass, that is imported here as MyClassFromModule, since we want to avoid name clashes later on in the file.

  2. declare global is what tells Typescript that any type declarations within are related to the global context, not to the current module (remember that the new file above is a module now, because of the top level import).

  3. namespace MyLib tells Typescript that the type declarations within apply to MyLib.

  4. export type MyClass = MyClassFromModule; exports the type MyClassFromModule, on the global scope, under MyLib. That is what makes code like var x: MyLib.MyClass; possible, since in this situation MyClass is resolved to a type.

  5. export const MyClass: typeof MyClassFromModule; exports the object MyClass of type MyClassFromModule, on the global scope, under MyLib. This is what makes code like var x = new MyLib.MyClass(); possible, since here, MyClass is an object (remember that, in Javascript, functions are objects).

  6. (<any>window).MyLib = { MyClass: MyClassFromModule }; defines a property MyClass in the MyLib object, which is in the global scope (i.e. window object, in the browser).

Notice that the code described on items 1 and 6 is actually the only thing that outputs Javascript code, as all the other statements are virtually just exporting definitions.

The MyLib.imports.ts file needs to be compiled along side with MyLib.ts because it contains part of the type definition used by MyLib.ts. You can easily add it to your tsconfig.json or to the source files if you are calling tsc directly. You can optionally use the compiler's --outfile to generate a single javascript file.

Great, we solved the type declarations for Typescript compilation. What do we do for things to work properly in the browser now? If you used --outfile (along side with --module amd) you will see the generated javascript looking similar to this:

define("mylib.imports", ["require", "exports", "node_modules/moduleA/index"], function (require, exports, moduleA_1) {  
    "use strict";
    exports.__esModule = true;
    window.MyLib = { MyClass: moduleA_1["default"] };
var MyLib;  
(function (MyLib) {
    var Test = (function () {
        function Test() {
        Test.prototype.doLibWork = function () {
            var x = new MyLib.MyClass();
        return Test;
    MyLib.Test = Test;
})(MyLib || (MyLib = {}));

You can see the compiler just concatenates the output of both input .ts files. The part, within the AMD define method is the MyLib.imports.ts as it is compiled as an AMD module, due to the top level import statement. Whatever comes after is the rest of the code in MyLib.ts, which is not a module.

The implementation for define needs to be provided by a module loader. You could use something like SystemJs. Importing our myLib.imports module would look like this, using SystemJs:


What this will do is look for the named module mylib.imports and, well, import it, this is, run the function registered in the define call. This call is asynchronous, though, which means that if MyLib is used before the asynchronous call completes, it is likely that undefined reference errors will start popping up.

This is the major practical reason why it makes little sense to try to consume a module from within the global scope, like this. I'll discuss a better alternative to the problem of keeping code in the global scope but still consuming modules in an upcoming post.

I've learned in the hard way what not to do. Hopefully this article might help others avoiding the same mistake.