TypeScript: Organizing your code with AMD modules and require.js

TypeScript has two methods of generating JavaScript output files: CommonJS, and AMD.  CommonJS is the default, and amd modules can be generated by adding the –module AMD option to the compiler flags.

In this post, I’ll show how to create and use AMD modules, as well as configuring require.js by using require.config.

Update

This article has been updated to use TypeScript 0.9.  You can download / browse the source at github/blorkfish/typescript-amd-require-0.9

The older 0.8.1 source for this solution can be found here.

The older 0.8.0 source for this solution can be found here

 

Creating a default project using CommonJS

Let’s start with a standard new TypeScript project – which by default creates an app.ts file, and a default.htm – and  add the following:

  • \app directory (for application files)
  • \app\classes (for our AMD classes)
  • \lib directory (for external libraries)
  • \modules (for our module definitions)
  • \app\AppMain.ts  ( note that you should remove any code that the compiler generates in this file)
  • \app\AppConfig.ts ( remove any code )
  • \app\classes\Greeter.ts ( remove any code )
  • download require.js and include it in the \lib directory. ( require.js can be found here release 2.1.8 )
  • download require.d.ts from DefinitelyTyped, and save it in the modules directory.

require_amd_1

\modules\Require.d.ts

As at the time of update, this is at version 2.1.1.

\app\classes\Greeter.ts as an AMD module

Cut the code defining the Greeter class from \app.ts into the \app\classes\Greeter.ts file:

Effectively, we are now starting to organise our project, with one .ts file for each class.

app\classes\Greeter.ts:
class Greeter {
    element: HTMLElement;
    span: HTMLElement;
    timerToken: number;

    constructor (element: HTMLElement) { 
        this.element = element;
        this.element.innerText += "The time is: ";
        this.span = document.createElement('span');
        this.element.appendChild(this.span);
        this.span.innerText = new Date().toUTCString();
    }

    start() {
        this.timerToken = setInterval(() => this.span.innerText = new Date().toUTCString(), 500);
    }

    stop() {
        clearTimeout(this.timerToken);
    }

}

Compiling the project now should show the error: Could not find symbo ‘Greeter’.

Let’s fix this first by using a CommonJS reference – add a reference path to app.ts:

app.ts
/// <reference path="app/classes/Greeter.ts" />

window.onload = () => {
    var el = document.getElementById('content');
    var greeter = new Greeter(el);
    greeter.start();
};

The project should now compile.

If you run the project now, (using Internet Explorer), the Greeter.js file will be unreferenced:

0x800a1391 – JavaScript runtime error: ‘Greeter’ is undefined

The simple solution is to include this new Greeter.js file in default.htm:

default.htm
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script type="text/javascript" src="app/classes/Greeter.js"></script>
    <script src="app.js"></script>
</head>

Running the project now will succeed:

typescript_amd_1

Converting Greeter.ts to an AMD module

TypeScript 0.9 and upwards will default to compile all source files as AMD compliant.  This is slightly different to 0.8 versions, where by default projects were compiled to commonJS.  For reference purposes, the following section shows how to use AMD in 0.8 versions.  If using TypeScript 0.9, please continue to the next section, Export Greeter.

Specifying AMD compilation for 0.8 and 0.8.1 versions of TypeScript:

To compile project files to AMD modules, unload your project file, edit it, and add the –module AMD option to the command line options:

0.8.1

:  Here is the 0.8.1 version of the project file:

Note that you will need to remove the –sourcemap option for Debug configuration, as sourcemap and AMD do not work well together.

  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <!--remove the --sourcemap option below-->
    <TypeScriptSourceMap></TypeScriptSourceMap>
  </PropertyGroup>
  <Target Name="BeforeBuild">
    <Message Text="Compiling TypeScript files" />
    <Message Text="Executing tsc$(TypeScriptSourceMap) @(TypeScriptCompile ->'&quot;%(fullpath)&quot;', ' ')" />
    <Exec Command="tsc$(TypeScriptSourceMap) --module AMD @(TypeScriptCompile ->'&quot;%(fullpath)&quot;', ' ')" />
  </Target>

Older version 0.8.0 compiler version:

  <Target Name="BeforeBuild">
    <Exec Command="&quot;$(PROGRAMFILES)\Microsoft SDKs\TypeScript.8.0.0\tsc&quot; --module AMD @(TypeScriptCompile ->'&quot;%(fullpath)&quot;', ' ')" />
  </Target>

Export Greeter

Before we change app\classes\Greeter.ts to an AMD module, have a look at the generated javascript source :

var Greeter = (function () {
    function Greeter(element) {
        this.element = element;
        this.element.innerHTML += "The time is: ";
        this.span = document.createElement('span');
        this.element.appendChild(this.span);
        this.span.innerText = new Date().toUTCString();
    }
    Greeter.prototype.start = function () {
        var _this = this;
        this.timerToken = setInterval(function () {
            return _this.span.innerHTML = new Date().toUTCString();
        }, 500);
    };

    Greeter.prototype.stop = function () {
        clearTimeout(this.timerToken);
    };
    return Greeter;
})();
//@ sourceMappingURL=Greeter.js.map

Now modify the Greeter class definition, and add the export keyword:

app/classes/Greeter.ts
export class Greeter {

AMD compliant javascript source.

After compiling, Note the changes to the javascript source – the entire code block has been wrapped in a define( […] ) block, and there is an extra exports.Greeter = Greeter; line at the bottom of the file:

define(["require", "exports"], function(require, exports) {
    var Greeter = (function () {
        function Greeter(element) {
            this.element = element;
            this.element.innerHTML += "The time is: ";
            this.span = document.createElement('span');
            this.element.appendChild(this.span);
            this.span.innerText = new Date().toUTCString();
        }
        Greeter.prototype.start = function () {
            var _this = this;
            this.timerToken = setInterval(function () {
                return _this.span.innerHTML = new Date().toUTCString();
            }, 500);
        };

        Greeter.prototype.stop = function () {
            clearTimeout(this.timerToken);
        };
        return Greeter;
    })();
    exports.Greeter = Greeter;
});
//@ sourceMappingURL=Greeter.js.map

Compiling at this stage will generate errors : Could not find symbol ‘Greeter’.

We now need to modify the app.ts file to import the the module.  Remove the ///reference path line, and add an import statement as below:

Now use the name of the import ( gt ) to reference gt.Greeter :

app.ts
import gt = module("app/classes/Greeter");

window.onload = () => {
    var el = document.getElementById('content');
    var greeter = new gt.Greeter(el);
    greeter.start();
};

Running the app at this stage now will produce the following error:

Unhandled exception at line 1, column 1 in http://localhost:8524/app.js

0x800a1391 – JavaScript runtime error: ‘define’ is undefined

This error is because define is part of the require.js library, as seen at the end of the require.d.ts file :

// Ambient declarations for 'require' and 'define'
declare var require: Require;
declare var requirejs: Require;
declare var req: Require;
declare var define: RequireDefine;

Configuring require.js

In order to use AMD modules, we need to tell our page to include require.js.  Looking at the require.js documentation, the way to do this is to include the following in your html page

default.htm :

<script data-main="app/AppConfig" type="text/javascript" src="lib/require.js"></script>

Note that the require.js syntax is to use the data-main property to specify a JavaScript file to load as the initial starting point for the application – in this case : app/AppConfig.js.

Remove the reference to Greeter.js, and app.js, so that your default.htm file looks like this:

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script data-main="app/AppConfig" type="text/javascript" src="lib/require.js"></script>
</head>
<body>
    <h1>TypeScript HTML App</h1>

    <div id="content"></div>
</body>
</html>

AppConfig.ts

Create an app/AppConfig.ts TypeScript file, as follows:

app/AppConfig.ts
/// <reference path="../modules/require.d.ts" />

import gt = module("classes/Greeter");

require([], () => {
    // code from window.onload
    var el = document.getElementById('content');
    var greeter = new gt.Greeter(el);
    greeter.start();
});

Note that we have moved the application startup code (the window.onload function) into the body of the require function.

The app should now run using AMD loading.

Adding further modules

Should your application require further AMD modules, simply include them in the /lib directory, and specify them in the first array  as follows:

app/AppConfig.ts
require(['../lib/jquery-1.7.2','../lib/underscore', '../lib/backbone', '../lib/console'], () => {
    // code from window.onload
    var el = document.getElementById('content');
    var greeter = new gt.Greeter(el);
    greeter.start();
});

Note that the paths for require are relevant to the location of the AppConfig.ts file (which is in the app directory).

Using require.config

require.js has a number of configuration options that make it so powerful.  Among these is the ability to define dependencies between modules.

Unfortunately, including a require.config in our app/AppConfig.ts file as shown below will result in a run-time error:

0x800a01b6 – JavaScript runtime error: Object doesn’t support property or method ‘config’

app/AppConfig.ts
/// <reference path="../modules/require.d.ts" />

// the config below will cause a run-time error
require.config({
    baseUrl: '../'
});

import gt = module("classes/Greeter");

require(['../lib/jquery-1.7.2','../lib/underscore', '../lib/backbone', '../lib/console'], () => {
    // code from window.onload
    var el = document.getElementById('content');
    var greeter = new gt.Greeter(el);
    greeter.start();
});

This run-time error is caused because the TypeScript compiler ( with –module AMD ) compile option wraps the entire file in a require statement.  Have a look at the generated code:

app/AppConfig.js (generated)
define(["require", "exports", "classes/Greeter"], function (require, exports, __gt__) {
    // this require.config below should NOT be inside the define function
    require.config({
        baseUrl: '../'
    });
    var gt = __gt__;

    require([
        '../lib/jquery-1.7.2', 
        '../lib/underscore', 
        '../lib/backbone', 
        '../lib/console'
    ], function () {
        var el = document.getElementById('content');
        var greeter = new gt.Greeter(el);
        greeter.start();
    });
})

Using require.config with AMD modules solution:

The solution here is to separate our require config file from our application main file, and remove any import module statements from the configuration file.  Remember how the generated javascript changed when we added the import statement to app/classes/Greeter.ts ? So make sure that the file with require.config does not have any import statements :

AppMain.ts

Create an AppMain.ts file within the app folder as follows:

app/AppMain.ts
import gt = module("classes/Greeter");

export class AppMain {
    public run() {
        var el = document.getElementById('content');
        var greeter = new gt.Greeter(el);
        greeter.start();

    }
}

Modify AppConfig.ts to use named require parameters

app/AppConfig.ts
/// <reference path="../modules/require.d.ts" />
/// <reference path="AppMain.ts" />
require.config({
    //baseUrl: '../' // commented for now
});

require(['AppMain', 
    '../lib/jquery-1.7.2','../lib/underscore', '../lib/backbone', '../lib/console' ], 
    (main) => {
    // code from window.onload
    var appMain = new main.AppMain();
    appMain.run();
});

Note that we have specified to require.js that it must load a file named ‘AppMain’ ( our AppMain.ts compiled file), and that when the require function runs, AppMain’s classes will be referenced by the named parameter main. If we were to name all of the parameters in the require array, our code would look something like this:

app/AppConfig.ts
require(['AppMain', 
    '../lib/jquery-1.7.2','../lib/underscore', '../lib/backbone', '../lib/console' ], 
    (main, $, _, console) => {
    var appMain = new main.AppMain();
    appMain.run();
});

Using named require parameters and shims

Fortunately, require.js allows us to configure named parameters via the paths config variable, and then define a shim property for each named path, that includes the export symbol, and any dependencies, as follows:

app/AppConfig.ts
require.config({
    baseUrl: '../',
    paths: {
        'jquery': 'lib/jquery-1.7.2',
        'underscore': 'lib/underscore',
        'backbone': 'lib/backbone',
        'console': 'lib/console'
    }, 
    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        },
        backbone: {
            deps: ["underscore", "jquery"],
            exports: "Backbone"
        },
        console: {
            exports: "console"
        }
    }
});

require([
     'jquery'
    , 'underscore'
    , 'backbone'
    , 'console'
    ], 
    ($, _, Backbone, console) => {
    $(() => {

        // code goes here

    });
});

Require.config tips

Note that for jquery plugins – all jquery extension methods must export to $ as well – this is accomplished by specifying $ as the export for plugins, and jquery as a dependency.  Note too that in the require function definition, TypeScript will not allow multiple parameters with the same name, so to use jquery.flip.js for example, use the following require.config:

require.config({
    paths: {
        'jquery': 'lib/jquery-1.7.2',
        'jqueryflip': 'lib/jquery-flip'
    },
    shim: {
        jquery: {
            exports: '$'
        },
        jqueryflip: {
            deps: ['jquery'],
            exports: '$'
        }
    }
});

require(['jquery', 'jqueryflip', 'AppMain'], ($, jqueryflip, main) => {
    var appMain = new main.AppMain();
    appMain.run();
});

Have fun,

- Blorkfish.

About these ads

36 Responses to TypeScript: Organizing your code with AMD modules and require.js

  1. Great article man! It helps me a lot!

  2. JcFx says:

    Good stuff. Really useful.

  3. Pingback: TypeScript: Organizing your code with AMD modules and require.js | coolmuse

  4. CCPony says:

    Great post. Question: how would you use TypeScript to name the resultant module? – - for example:

    define (“NamedModule”, ["require", "export"], function (require, export) {..}

    Thanks Blork – - CCPony

  5. joeriks says:

    Great post, thanks for sharing. A question: did you try to put the RequireJS optimizer into this, or some other bundle&minifier?

  6. Jed Hunsaker says:

    Hmm… the only problem I have with this is once you enter the “// code goes here” block, you’ve lost all of the Intellisense you would get from a /// at the top.

    • Jed Hunsaker says:

      The comment system cut out my HTML tag above, but I’m trying to say if you reference jquery.d.ts at the top, you overwrite it with the $ down below, losing all Intellisense (code hints) in Visual Studio.

    • Jed Hunsaker says:

      Aha! OK – put the reference path=”../modules/jquery.d.ts” up at the top and then down below, replace the $ sign with $:JQueryStatic. Then you get all the Intellisense! Yay!

  7. Robin says:

    I was getting along well with this until the Configuring RequireJS section. Now I get an error saying “Module name “classes/Greeter” has not been loaded yet for context: _. Use require([])”

    I downloaded the full source solution and get something similar:
    “Module name “app/classes/Greeter” has not been loaded yet for context: _”

    Any ideas? I’d really appreciate any suggestions.

    • blorkfish says:

      Hi Robyn,
      I was able to reproduce your problem – and it seems that it has to do with the newest version of the TypeScript compiler (0.8.1 and up). I have updated this blog, and also included a 0.8.1 version of the sample app.
      In essence, it was caused by not having the –module AMD compiler flag. Have a look a the modifications to the project file for the correct syntax. Also, it seems that sourcemaps are not working with AMD modules, so you will need to remove this flag from the compile options.
      Have fun,
      Blorkfish

  8. Pingback: Modularization in TypeScript « Keyhole Software

  9. Pingback: Modularization in TypeScript | Brett Jones, Dev.

  10. Brett Jones says:

    Hey all,

    I’ve been looking into this whole TypeScript/JS AMD stuff and wrote a blog post on the subject. I found a fairly convenient way to do what you’re doing here, only without introducing the AMD/requireJS syntax into your TypeScript code, which I think is definitely a plus.

    I’d love to hear your thoughts – check it out at http://brettjonesdev.com/modularization-in-typescript/

    Brett Jones – @brettjonesdev

  11. Luke Ryan says:

    Great post.. really helpful!

  12. Thnx, Just picked up TS a few days ago and this is the first real helpful documentation on TypeScript and implementing AMD that I have come across so far. Now I am off to go and read that Ioc for typescript article…

  13. deadcabbit says:

    Setting up r.js building is not a big deal, but with no conditional compilation in the htm file generating a production html and a dev html ends up pretty messy in my opinion (I’m not a .net developer so there may be better ways; I would use an afterBuild script possibly run via node). Using require.js without building is pointless (at least outside development).

    What I don’t like in the above code is that we’re still using named functions (Greeter in this case), a returnable anonymous “class” would not work, though that would be the “AMD way” (export class {…) – of course one may use something like “export class Klass”, but probably there’s a better way, I’m just not familiar enough with ts.

  14. Arek Bal says:

    Guys, you are not “required” to use require.d.ts. Script ‘require.js’ is enough
    calling require with callback in *.ts makes u lose type information anyway.

    Proper way of doing it are module declarations like that one:
    import blogger = module(‘Blogger’)

    which during generation encloses rest of code in proper define() block.

    Keep in mind that:
    1. Module load statement above can use a .ts filename where ‘Blogger’ is because ts files without module block inside are modules(like in python). But… check a line below…
    2. In order to have AMDable module you should not enclose your code in non-exportable module at top-level.

  15. Trong says:

    Identifier expected.
    I got this issues ……

    • Jed Hunsaker says:

      That issue means you’re missing a token of some kind. Could be a semi-colon or a brace.

      • Trong says:

        I dowloaded the sample code …
        declare module require
        {
        export function (requires: string[], f: Function);
        export function (requires: string);
        export function config(require: any);
        };

        These errors at requires

      • blorkfish says:

        Hi Trong,
        The issue you were having is because this article was written targeting TypeScript 0.8, and the require.d.ts definitions that I originally included are not 0.9 compatible. I’ve since updated the article to work with TypeScript 0.9. Have a look at the top of the article for a link to a new github repository where you can download the latest source.
        Have fun, blorkfish

  16. Trong says:

    @blorkfish: thank for the update, but I got interesting issue here: If I set a firefox as default browser and run the app the first time the code was not running
    “The time is: ………..” this line was not display. I left the browser open and rerun again it would run. If I hit refresh I got this errors:

    Uncaught Error: Module name “classes/Greeter” has not been loaded yet for context: _. Use require([])
    http://requirejs.org/docs/errors.html#notloaded require.js:138
    Uncaught TypeError: Cannot read property ‘AppMain’ of undefined

    Kind of disappoint with the TypeScript :( …..

  17. Jesus Christ :( All this mess for using modules… Began to read anyway.

  18. Good tutorial. Completed it and now I understand, how the things work.

  19. Rad says:

    Can this method be used with Angularjs modules. If so any examples.
    Thanks

  20. SergejPopov says:

    Two years later, and still useful article. Thanks!

    Note: `import gt = module(“classes/Greeter”);` now has to be `import gt = require(“classes/Greeter”);` otherwise compiler will throw “Module cannot be aliased to a non-module type”

    • blorkfish says:

      Hey Sergej,
      I’me glad you found the article helpful. It’s always good to hear positive feedback.
      Thanks too for the updated syntax.
      The article has been updated quite a few times, as it started with TypeScript 0.8, through 0.8.1, then the 0.9.1 range.
      I’ll make a note in the main body of text as soon as I can.

  21. David Fowler says:

    Your first default.htm file is missing a div with id=”content”

  22. Pingback: Modularization in TypeScript | Keyhole Software

  23. Very nice article, thanks a lot, I learnt several things! I encounter a problem related to the last very interesting paragraph: what if I need to load 2 jQuery plugin in a module regarding the issue with $ exported twice… I would be very interested to know if you have a typical way of proceeding for that case. I am currently thinking about tweaking module or create mine without typescript syntax…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: