Recently, we’ve been moving our Browser RUM agent from JavaScript to TypeScript.Let me share a little of how we migrated to TypeScript, some of the difficulties that arose and how we tackled them.
Before moving to TypeScript, our Browser RUM agent had thousands lines of code, but was suppressed in to just two JavaScript file
We felt obligated to refactor it before doing 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:
Why TypeScript
We felt obligated to refactor it before doing 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:
- Static Typing
- Module and Classes
- Superset of JavaScript, easier to learn for JavaScript developers
- Success story from our front-end team
Now is 8 Steps to Migrating to TypeScript...!!!
Prepare Yourself
- TypeScript Official Web Site is the best start
- TypeScript Succinctly is a good book for free
- If you are already a JavaScript developer, it feels smooth to pick the knowledge up
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.
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
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.
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.
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.
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.
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.
- 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.
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() { }
}
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'>: {>…}
};
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.
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);
Thanks...!!!