The Benefits of Migrating from JavaScript to TypeScript

Recently, we moved our Browser RUM agent from JavaScript to TypeScript. In my last post, I focused on walking through the steps of migrating from JavaScript, the challenges, and best practices we uncovered along the way.

This one will focus on more details of the benefits and one missing feature in TypeScript compiler we suggest to implement.

TypeScript’s main benefits:

  1. Class and Module Support
  2. Static Type-checking
  3. ES6 Feature Support
  4. Clear Library API Definition
  5. Build-in Support for JavaScript Packaging
  6. Syntax Similarity to Our Backend Languages (Java, Scala)
  7. Superset of JavaScript

Class and Module Support

Keywords like class, interface, extends and module are available in TypeScript.
You can define a class as

// class define in TypeScript
class VirtualPageTracker extends Tracker {
    private virtualPageName: string = '';
    constructor(name) {
        super(name);
    }

    getName(): void {
        return this.virtualPageName;
    }

    static getTrackerName(): string {
        return  'VirtualPageTracker';
    }
}

TypeScript compiler will transcompile it to

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d

= b

; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; // class define in TypeScript var VirtualPageTracker = (function (_super) { __extends(VirtualPageTracker, _super); function VirtualPageTracker(name) { _super.call(this, name); this.virtualPageName = ''; } VirtualPageTracker.prototype.getName = function () { return this.virtualPageName; }; VirtualPageTracker.getTrackerName = function () { return 'VirtualPageTracker'; }; return VirtualPageTracker; })(Tracker);

Static Type-checking

TypeScript compiler will check the type (to surface more typing errors at compiling time)

var name: string;
name = 2; // type error, assign a number to a string type variable

function foo(value: number) {}
foo(''); // type error, use a number as a string type parameter

interface Bar {
    setName: (name: string) => void;
    getName: () => string;
}


var bar: Bar = {
    getName: function() {
        return 'myName';
    }
} // type error, setName function is missing in the object assigned to bar.

A practical example is if we use the wrong data type in the browser beacon, we now get compiling errors. Before migrating to Typescript, they could only be found by testing against the back-end.

ECMAScript 6 Feature Support

It is the current version of the ECMAScript Language Specification with more language features.
With TypeScript, you can start using many ES6 features although it may not be supported in your target browser. TypeScript compile can compile the ts files into “ES3”, “ES5” or “ES6”.

Some of the features are very handy like:

// for..of loops
var arr = ['a', 'b', 'c'];
for (let item of arr) {
    console.log(item);
}

It’s compiled to

// for..of loops
var arr = ['a', 'b', 'c'];
for (var _i = 0; _i < arr.length; _i++) {
    var item = arr[_i];
    console.log(item);
}

Refer to TypeScript ES6 Compatibility Table for more ES6 features you can use.

Clear API Definition

To let other TypeScript libraries use your library, you need to create a .d.ts file to declare all your public types and APIs of your library.
These definition files turned out to be clear and accurate references of your public APIs since they are always maintained and update-to-date because
you always need them if you write your tests in TypeScript too.

Refer to https://github.com/borisyankov/DefinitelyTyped for TypeScript definition files created for large amounts of JavaScript libraries.

Build-in Support for JavaScript Packaging

You can define one main entry ts file and refer all the ts files you need in the output js file.

Compiling the main entry ts file with the –out option, the compiler will concatenate all the directly or indirectly referred ts files into one js file in the order they are referred.

Thus, we can easily tailor our library into multiple versions.
For example, with the same code base, we are able to generate specific versions of browser agents for desktop and mobile respectively.
We just need to create one entry file for each version with different ts files referred in it.

Syntax Similarity to Our Backend Languages (Java, Scala)

We use Java and Scala at the back-end.
TypeScript’s similarity to these languages allows our developers can switch between front-end and back-end programming more smoothly.
Refer http://www.slideshare.net/razvanc/quick-typescript-vs-scala-sample for a quick syntax comparison between TypeScript and Scala.

Superset of JavaScript

As a superset of JavaScript, TypeScript has a smooth learning curve for JavaScript developers.
This means  you can adopt TypeScript in your existing JavaScript projects quickly in an less disruptive way than CoffeeScript or ClojureScript.

One Missing Feature Suggested

In addition to the benefit, we also found some missing features could be implemented.
One of them is to merge the same module into the same function rather than multiple functions.

module A {
    function foo() { }
}

module A {
    function bar() {
        foo();
    }
}

generates below code with compiling error “cannot find name ‘foo’”.

var A;
(function(A) {
    function foo() {}
})(A || (A = {}));
var A;
(function(A) {
    function bar() {
        foo();
    }
})(A || (A = {}));

foo function defined within the first anonymous function call for module A is not visible in the second anonymous function call, so you have to export it as:

module A {
    export function foo() {}
}

module A {
    function bar() {
        foo();
    }
}

generates below code without error:

var A;
(function(A) {
    function foo() {}
    A.foo = foo;
})(A || (A = {}));
var A;
(function(A) {
    function bar() {
        A.foo();
    }
})(A || (A = {}));

The problem here is now A.foo is not only visible to module A. Anyone can call it and modify it now.

There is no module level visible concept which should be similar to Java’s “package-private” when there is no modifier for Java classes or members.

This could be solved by generating:

module A {
    export function foo() {}
}

module A {
    function bar() {
        foo();
    }
}

to

var A;
(function (A) {
    function foo() { }
    A.foo = foo;
})(A || (A = {}));
var A;
(function (A) {
    function bar() {
        A.foo();
    }
})(A || (A = {}));

The problem of merging into one function is a potential name conflict between the same module in two files. But the compiler can report error in this case, and if two people are working independently on the same module in two files, it would be better to create two different sub modules. Merging into one function could be a feasible way support module level visibility.

As I write this article, I notice the /* @internal */ annotation in the ts compiler source code; it’s an experimental option released with typescript 1.5.0-alpha to strip the declarations marked as @internal.

It helps to only include the declarations without @internal (which serves as your external APIs) when generating the .d.ts file from your code. And if your consumer are using TypeScript too, this prevents it from using your internal members.

Generating the .d.ts file for:

module A {
    /* @internal */ export function internal() {}
    export function external() {}
}

by

tsc -d --stripInternal A.ts

will output

declare module A {
    function external(): void;
}

However, if your consumers uses JavaScript, they can still use the internal function.

Conclusion

By and large, it’s a pleasant and rewarding experience to move to TypeScript. Though it adds limitations on your JavaScript implementation, you can either find a good workaround or implement the benefits that outweigh it. Moreover, it’s an active open source project (about 200 commits to master in last month) with well documentation to help you start easily. And just in March this year, Google also announced they would replace AtScript with TypeScript. Angular 2 is now built with TypeScript too. So far, the move to TypeScript has proved beneficial.

8 Steps to Migrating from JavaScript to TypeScript

Recently, we’ve been moving our Browser RUM agent from JavaScript to TypeScript. Though it’s been a challenge, we enjoyed seeing how the change will benefit us and it’s been fun learning a new language in the process. Let me share a little of how we migrated to TypeScript, some of the difficulties that arose and how we tackled them.

Why TypeScript

Before moving to TypeScript, our Browser RUM agent had thousands lines of code, but was suppressed in to just two JavaScript files.

We felt obligated to refactor it before doing any real work, to make our life easier when adding additional features. Having experienced the pain of developing a large scale app in JavaScript, we decided to take a shot at their sibling languages that have better support for large-scale development.

After looking into languages such as TypeScript, CoffeeScript, and PureScript, we decided to go with TypeScript for a few reasons:

  1. Static Typing
  2. Module and Classes
  3. Superset of JavaScript, easier to learn for JavaScript developers
  4. Success story from our front-end team

8 Steps to Migrating to TypeScript

  1. Prepare Yourself

  1. Rename Files

We renamed all the js files to ts files and as TypeScript is just a superset of JavaScript, you can just start compiling your new ts files with the TypeScript compiler.

  1. Fix Compiling Errors

There were quite a few compiling errors due to the static type checking by the compiler. For instance, the compiler will complains about js code below:
Example One

var xdr = window.XDomainRequest;

Solution

// declare the specific property on our own
interface Window {
    XDomainRequest?: any;
}

Since “XDomainRequest” is an IE only property to send cross domain request, it’s not declared in the “lib.d.ts” file (it’s a file declaring the types of all the common JavaScript objects and APIs in the browser and it’s referenced by typescript compiler by default).
You will get “error TS2339: Property ‘XDomainRequest’ does not exist on type ‘Window’.”.
The solution is to extend the Window interface in “lib.d.ts” with an optional “XDomainRequest” property.

Example Two

function foo(a: number, b: number) {
    return;
}

foo(1);

Solution

// question mark the optional arg explicitly
function foo(a: number, b?: number) {
    return;
}

Optional function args need to be marked explicitly in typescript, or it gives “error TS2346: Supplied parameters do not match any signature of call target.”.
The solution is to explicitly use “?” to mark the parameter as optional.

Example Three

var myObj = {};
myObj.name = "myObj";

Solution

// use bracket to creat the new property
myObj['name'] = 'myObj';
// or define an interface for the myObj
interface MyObj {
    name?: string
}

var myObj: MyObj = {};
myObj.name = 'myObj';

When assign an empty object “{}” to a variable, typescript compiler infers the type of the variable to be empty object without any property.
So accessing “name” property gives “error TS2339: Property ‘name’ does not exist on type ‘{}'”.
The solution is to declare an interface with an optional “name” property for it.

It’s kind of fun to fix these errors and you learn about the language and how the compiler can help.

  1. Fix Test Cases

After successfully getting a JavaScript file from those ts files, we ran the tests against the new JavaScript files and fixed all the failures.

One example of the test failures caused by moving to TypeScript is the difference between these two ways of exporting a function:

export function foo() {}
export var foo = function() {}

Assuming your original JavaScript code is:

var A = {
    foo: function() {},
    bar: function() {foo();}
}

The test case shows:

var origFoo = A.foo;
var fooCalled = false;
A.foo = function(){fooCalled = true;};
A.bar();
assertTrue(fooCalled);
A.foo = origFoo;

If the TypeScript rewrite for the JavaScript is:

module A {
    export function foo() {}
    export function bar() {foo();}
}

The test case will fail. Can you tell why?

If you look at the generated JavaScript code, you will be able to see why.

// generated from export function foo() {}
var A;
(function (A) {
    function foo() { }
    A.foo = foo;
    function bar() { foo(); }
    A.bar = bar;
})(A || (A = {}));

In the test case, when the A.foo is replaced, you are just replacing the “foo” property of A but not the foo function, the bar function still calls the same foo function.

export var foo = function(){}

can help here.

TypeScript

module A {
    export var foo = function () { };
    export var bar = function () { foo(); };
}

generates

// generated from expot var foo = function() {}
var A;
(function (A) {
    A.foo = function () { };
    A.bar = function () { A.foo(); };
})(A || (A = {}));

Now we can replace the foo function called by A.bar.

  1. Refactor Code

TypeScript Modules and Classes help organize the code in a modularized and object-oriented way. Dependenies are referenced in the file header.

///<reference path=“moduleA.ts” />
///<reference path=“moduleB.ts” />
module ADRUM.moduleC.moduleD {
    ...
}

One thing I like when compiling a ts file is using the “–out” option to concatenate all the directly or indirectly referenced ts files, so I don’t need to use requirejs or browserify for the same purpose.

With TypeScript, we can define classes in a classical inheritance way rather than the prototypal inheritance way which is more familiar to Java and C++ programmers. However, you lose the flexibility JavaScript provides too.

For example, if you are seeking a way to hide a function in the class scope, save your time, it isn’t supported. The workaround is to define the function in the module and use it in the class.

TypeScript allows you to define modules and classes in an easy way and generates the idiomatic JavaScript for you. As a result, I feel like you may also have less opportunities to learn more advanced JavaScript knowledge than programming in pure JavaScript.

But just like moving from assembly to C/C++, by and large, it’s still a good thing.

We did not bother adding all the type information in the existing code, but we’ll need to do it when changing or adding code.

It is also worth moving the test cases to TypeScript, as the test cases could be auto updated when refactoring the code in the IDE.

  1. Fix Minification

Don’t be surprised if the minification is broken especially when you use Google Closure Compiler with advanced optimization.

Problem 1: Dead Code Mistakenly Removed

The advanced optimization has a “dead code removal” feature that removes the code which recognized as unused by the compiler.

Some early version closure compiler (i.e. version 20121212) mistakenly recognizes some code in TypeScript modules as unused and removes them. Fortunately, it’s been fixed in the latest version compiler.

Problem 2: Export Symbols in Modules

To tell the compiler not to rename the symbols in your code, you need to export the symbols by the quote notation. It means you need to export the API as shown below to allow the API name to stay constant even with the minified js file.

module A {
    export function fooAPI() { }
    A["fooAPI"] = fooAPI;
}

transpiled to:

var A;
(function (A) {
    function fooAPI() { }
    A.fooAPI = fooAPI;
    A["fooAPI"] = fooAPI;
})(A || (A = {}));

It’s a little bit tedious. Another option is to use the deprecated @expose annotation.

module A {
    /**
    * @expose
    */
    export function fooAPI() { }
}

This looks like it will removed in future, and hopefully you might be able to use @export when it’s removed. (Refer to the discussion at @expose annotation causes JSC_UNSAFE_NAMESPACE warning.)

Problem 3: Export Symbols in Interfaces

If you define a BeaconJsonData interface to be passed to other libraries, you’ll want to keep the key names.

interface BeaconJsonData {
    url: string,
    metrics?: any
}

@expose does not help as the interface definition transpile to nothing.

interface BeaconData {
    /**
    * @expose
    */
    url: string,
    /**
    * @expose
    */
    metrics?: any
}

You can reserve the key names by quote notation:

var beaconData: BeaconData = {
    'url': "www.example.com",
    'metrics': {}
};

But what if you want to assign the optional key later?

var beaconData: BeaconData = {
    'url': "www.example.com"
};

// ‘metrics’ will not be renamed but you lose the type checking by ts compiler
// because you can create any new properties with quote notation
beaconData["metrics"] = {…};
beaconData["metricsTypo"] = {}; // no compiling error

// ‘metrics’ will be renamed but dot notation is protected by type checking
beaconData.metrics = {…};
beaconData.metricsTypo = {…}; // compiling error

What we did is to expose the key name as
/** @expose */ export var metrics;
in the interface file to prevent the closure compiler from renaming it.

  1. Auto-Generate Google Closure Compiler Externs Files

For the Closure Compiler, if your js code calls external js library’s APIs, you need to declare these APIs in an externs file to tell the compiler not to rename the symbols of these APIs. Refer to Do Not Use Externs Instead of Exports!

We used to manually create the externs files and any time we use a new API, we have to manually update its externs file. After using TypeScript, we found that TypeScript .d.ts and the externs file have the similar information.

They both contain the external API declarations — .d.ts files just have more typing information — so we can try to get rid of one of them..

The first idea came into my mind is to check if the TypeScript compiler supports minification. As the ts compiler understand the .d.ts file, it won’t need the externs files. Unfortunately, it doesn’t support it, so we have to stay with the Google Closure Compiler.

Then, we thought the right thing is to generate the externs files from the .d.ts files. Thanks to the open source ts compiler, we use it to parse the .d.ts files and convert them to externs file (see my solution at https://goo.gl/l0o6qX).

Now, each time we add a new external API declaration in our .d.ts file, the API symbols automatically appears in the externs file when build our project.

  1. Wrap the ts code in one function

Ts compiler generates code for modules like below:

// typescript
module A {
    export var a: number;
}

module A.B {
    export var b: number;
}

// transpiled to javascript
var A;
(function (A) {
    A.a;
})(A || (A = {}));

var A;
(function (A) {
    var B;
    (function (B) {
        B.b;
    })(B = A.B || (A.B = {}));
})(A || (A = {}));

For each module, there is a variable created and a function called. The function creates properties in the module variable for exported symbols. However, sometimes you want to stop execution for some conditions such as your libraries need to be defined or it has been disabled, you need to wrap all the js code in a function by yourself, like:

(function(){
    if (global.ADRUM || global.ADRUM_DISABLED) {
        return;
    }

    // typescript generated javascript goes here

}(global);