Saturday, 20 October 2012

Asynchronously loading Javascript library in TypeScript

What's the problem?

In TypeScript, one way to use "native" Javascript libraries (like JQuery, Underscore, etc...) is to declare ambient variables in your code. The problem with these ambient variables is that you need to reference the corresponding Javascript files in your html when you want to use them. Since I am using AMD to load my modules, I would prefer using the same asynchronous mechanism to load existing Javascript libraries than having to reference all the libraries in my html. This post will describe the solution I found to this problem.


Here is an example if you want to use the library Underscore.js in a Typescript file with the help of ambient variables.


default.html:
<head>
    <meta charset="utf-8" />
    <script src="underscore.js"></script>
    <script src="require.js"></script>
</head>

test.ts:

/// <reference path='underscore.d.ts'/>
declare var _: underscore;
export class Test {
    executeTest() {
        _.each([1, 2, 3], function (num) { alert(num); });
    }
}
Reference: Underscore.d.ts.

As you can see, I need to reference underscore.js in my html. If I had to use JQuery, the same thing would apply. But I would rather have something like this:

default.html:
<head>
    <meta charset="utf-8" />
    <script src="require.js"></script>
</head>

test.ts:

import _ = module('underscore');
export class Test {
    executeTest() {
        _.each([1, 2, 3], function (num) { alert(num); });
    }
}

In this example, you only need to include require.js in the html and import the module underscore. Obviously, that doesn't work because underscore is not a valid TypeScript module. 

Here is the solution...


The solution I found is not as clean as the previous example but it's as close as I can get. You can find the source on GitHub.


Here is the file structure:


\scripts\app.js
\scripts\test.ts
\scripts\underscoreLib.ts
\scripts\libs\underscore.js
\scripts\libs\require.js
\default.htm

And that's the code:

default.html:

<head>
    <meta charset="utf-8" />
    <script src="require.js" data-main='scripts/app'></script>
</head>

app.js:
require.config({
    paths: {
        'underscoreLib': 'libs/underscore'
    },
    shim: {
        'underscoreLib': {
            exports: '_'
        }
    }
});

underscoreLib.ts
export function each(obj: any, f: Function) { }

test.ts
import underscore = module('underscoreLib');
var _:underscore = underscore;

export class Test {
    executeTest() {
        _.each([1, 2, 3], function (num) { alert(num); });
    }
}

How it works?

The first trick is to use shims with the Require.JS api. This basically lets us define the name of a module and associate it to the Javascript library we want to load. You can see in app.js that I define a module named 'underscoreLib' with the path = 'libs/underscore.js'. You also need to define what variable will be exported when the module will be loaded. In our case, that's the '_' of Underscore.JS, but for JQuery, that would be '$'.

In pure Javascript, you would then use the define() method to load the underscoreLib module like like this:


define(['underscoreLib'], function(_){
    _.each([1, 2, 3], function (num) { alert(num); });
})

The goal is to find a way in TypeScript to make the compiler generates a javascript file that follows this pattern. If you look at a generated Javascript file when you import a module in a TypeScript file, you will see that the require() method is used just like we want. So the other trick is to create a TypeScript file with the same name as the module name we used in the require shim. In our case, that would be underscoreLib. 

If you look at the test.ts file, we import the module named 'underscoreLib'. Since we created the file named underscoreLin.ts the compiler knows that the module exist and it compile correctly. But then if you look at the Javascript, you have something like this:

define(["require", "exports", 'underscoreLib'], function(require, exports, __underscore__) {
    var underscore = __underscore__;

    var _ = underscore;
    var Test = (function () {
        function Test() { }
        Test.prototype.executeTest = function () {
            _.each([1, 2, 3], function (num) { alert(num); });
        };
        return Test;
    })();
    exports.Test = Test;    
})

Exactly what we wanted. When Require.JS will try to load the underscoreLib module, it will use the shim config to find the Javascript file associated to it. When loaded, the 'underscore' variable will contains the '_' of the Underscore.JS library. To make it more natural to use, you can declare a variable '_' like this:


var _:underscore = underscore;



Note that if you want to use your Javascript library in a strong-typed manner, you must declare all methods in the TypeScript file corresponding to your module. In my example, I only declared the method 'each()' in the underscoreLib.ts file, but I would need to declare all the methods I want to use if I want my project to compile. If you don't care about strong-typing for your library, you can declare the '_' variable as a 'any' type like this:


var _:any = underscore;

In this case, you only need to export at least one method in the underscoreLib.ts, otherwise the compiler won't generate the module and the code won't compile.


Et voilĂ ! I hope this can help other people. Let me know if you find a better way to do it.

2 comments:

  1. Very nice! I've been looking into TypeScript modularization techniques and just wrote a blog post of my own based on my findings (my post cites this one I might add).

    I found a way to import existing JS libraries which allows you to leverage the interface definitions which are easily found online, without having to write your own like you do here.

    Check it out and let me know what you think! http://brettjonesdev.com/modularization-in-typescript/


    @brettjonesdev

    ReplyDelete
    Replies
    1. Thanks for sharing. I'll definitely take a look at your solution and see how it works.

      Delete