Software Imagineer's blog

Incrementally migrating JavaScript AMD project to TypeScript

Sat Jan 21 2017

Asynchronous Module Definition (AMD) and RequireJS was my choice for Javascript code modularization for last four years. But enterprise web application I am working on is growing and it's harder and harder to manage the code base. I did some technical research and decided to migrate the project to TypeScript.

TypeScript ticked all the bullet points in my wish list:

  • Support AMD format, so code can be migrated incrementally
  • JavaScript is legal TypeScript
  • Type checking for stricter interfaces
  • ES6 while we wait for it to happen (or some of it)
  • Superb IDE support to enforce all this magic

TypeScript seemed like a perfect solution for my problem, but my web app was way too big and complex to rewrite to TypeScript in a big bag fashion. I had to do incremental approach and commit to write all new code in TypeScript, while keeping old code in JavaScript. This interchangeable use of TypeScript and JavaScript caused some problems, which required poorly documented TypeScript features, which I am going to describe in this blog post.

For people who like to check source code and skip the reading: https://github.com/v3nom/RequireJStoTypescript

Consuming TypeScript from JavaScript (AMD)

Consuming TypeScript code from JavaScript is pretty straight forward. Compile Typescript source code using AMD module format. Generated JavaScript is by default placed next to TypeScript files, and this setup worked for me best so far.

Check out new tsconfig.json feature (read more).

//tsconfig.json
...
"compilerOptions": {
    "module": "amd",
}
...

TypeScript class we want to use in existing JavaScript code. Class has to be exported as module (external module in TS 1.4).

// src/ts/advancedMathOperations.ts
class AdvancedMathOperations {
    multiply(a: number, b: number) {
        return a * b;
    }
}
export = AdvancedMathOperations;

Simple JavaScript module. We expect that JavaScript file was generated in the same place as TypeScript source, so we don't really need to think about it when declaring AMD dependencies.

// src/main.js
require([‘mathOperations’, ‘ts/advancedMathOperations’], function(MathOperations, AdvancedMathOperations) {
 console.log(‘JS amd: ‘, MathOperations.sum(1, 2));
 var advancedMathOperations = new AdvancedMathOperations();
 console.log(‘TS amd: ‘, advancedMathOperations.multiply(3, 5));
});

Consuming existing JavaScript AMD from TypeScript

Now this is where things get a little bit more tricky. In order to import JavaScript code we need to use reference comments. JavaScript module we want to use in TypeScript code.

// src/utils/logger.js
define(function(){
    return function(){
        var self = {};
        self.log = function(msg){
            console.log(‘Log: ‘,msg);
        };
    
        return self;
    }
});

TypeScript file using legacy JavaScript code. Reference comment has to be first thing in TypeScript file (gotcha). You basically write what will be filled in AMD define when JavaScript code will be generated: path is dependency location and name is name by which you can reference imported dependency.

/// <amd-dependency path=”utils/logger” name=”Logger” />
// src/ts/advancedMathOperations.ts
declare var Logger:any;
class AdvancedMathOperations {
    logger;
 
    constructor() {
        this.logger = new Logger();
    }

    multiply(a: number, b: number) {
        this.logger.log(`Operation ${a} * ${b}`)
            return a * b;
        }
    }
}
export = AdvancedMathOperations;

Compiled JS code explains best what happened.

define([“require”, “exports”, “utils/logger”], function (require, exports, Logger) {
    var AdvancedMathOperations = (function () {
        function AdvancedMathOperations() {
            this.logger = new Logger();
        }
        AdvancedMathOperations.prototype.multiply = function (a, b) {
            this.logger.log(“Operation “ + a + “ * “ + b);
            return a * b;
        };
        return AdvancedMathOperations;
    })();
    return AdvancedMathOperations;
});

Bonus: Type support for legacy JavaScript code (manual)

You can also get type checking for legacy JavaScript code in TypeScript. If you have some spare time to declare type definitions.

// typed/demo.d.ts
declare class Logger {
    log(msg:string);
}

And now we can simplify our previous TypeScript class

/// <amd-dependency path=”utils/logger” name=”Logger” />
// src/ts/advancedMathOperations.ts
class AdvancedMathOperations {
    logger;
 
    constructor() {
        this.logger = new Logger();
    }
    multiply(a: number, b: number) {
        this.logger.log(`Operation ${a} * ${b}`)
        return a * b;
    }
}
export = AdvancedMathOperations;

Exactly the same JavaScript code will be generated. If you have problems with name collisions in your project you can also update old version with:

declare var Logger:Logger; 

Conclusion

Incrementally updating enterprise web application to TypeScript was a breeze. Every time I would touch a legacy JavaScript code I would convert it to TypeScript. And since JavaScript is legal TypeScript there was very low chance of introducing new bugs. But of cource unit tests are a must in any big project.

From now on all new client code is written in TypeScript and it was quite easy to get other team members on board.