Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Incrementally Migrating JavaScript AMD Project to TypeScript

DZone's Guide to

Incrementally Migrating JavaScript AMD Project to TypeScript

A step-by-step guide to migrating web apps to TypeScript, and the reasoning behind these changes.

· Web Dev Zone
Free Resource

Tips, tricks and tools for creating your own data-driven app, brought to you in partnership with Qlik.

Motivation

Asynchronous Module Definition (AMD) and RequireJS was my choice for Javascript code modularization for last four years. But the 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 best for me so far.

Check out the 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 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;
 }
});


This is a TypeScript file using legacy JavaScript code. The reference comment has to be first thing in TypeScript file (gotcha). You basically write what will be filled in AMD define when the JavaScript code will be generated: path is the dependency location and name is the name by which you can reference an 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 best explains 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 course, 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.

Explore data-driven apps with less coding and query writing, brought to you in partnership with Qlik.

Topics:
javascript ,typescript ,requirejs ,web dev

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}