Converting JavaScript to TypeScript

JavaScript to TypeScript

So perhaps you and/or your development team have been looking at TypeScript and have decided it would be a nice tool to leverage in development.

Your team is sick of having to track down how your JavaScript objects are composed, sick of having to reference third party library documentation every waking moment.

Wouldn’t it be great if you didn’t have to spend most of your time debugging runtime errors?

All of these are great reasons to consider moving your project to TypeScript.

“But Mike”,  you say, “I already have thousands of lines of JavaScript code!”

No sweat! I can guide you through the conversion process!  Believe me, while it may take some time, it is not as painful as you may believe.

Why TypeScript

Hopefully you have some idea of why you would like to switch to using TypeScript, but I will go over some of the great advantages of the language.

Static Typing. IDEs are able to give you context-sensitive suggestions for the types you are using, no more having to second guess the name of a method on a type!

The DefinitelyTyped repository has a large collection of “typings” (files that give the IDE/Transpiler static typing) for thousands of libraries that have been written in JavaScript.

Refactoring Support. Most IDEs are able to take advantage of the static typing TypeScript provides and offer you a reliable way to refactor your types. (No more error-prone find and replacing!)

Faster Development in Large Projects. For smaller projects that are within everyone’s purview, dynamic typing may be fine.  When you toss in half a dozen developers, go through staffing changes, it’s hard to keep up with what is going on in the code.

It is a great convenience to be able to add abstractions such as interfaces and abstract implementations to your types, which will help you ensure developers are following the correct patterns for development.

Next Generation JavaScript. TypeScript has been developed to have features in line with the future specifications of ECMAScript.  They have been able to provide great conveniences such as await/async support available for use in your code today.

Let’s Get Started!

Now let’s begin the fun part of converting our project to TypeScript.  Remember the end in sight will be worth the journey of conversion, and don’t forget to take breaks so you don’t turn into a JavaScript converting android.  The conversion process is not necessarily hard, but depending on your project, it may be lengthy.

Project Setup

I will assume you have npm which is part of nodejs installed.  Note installing typing packages through npm is now the recommended way, however you may use Nuget packages, bower, or manually install DefinitelyTyped packages.

I will be using Knockout in this example since it is a lean binding library, and thus a simple way for us to get started.

Issue these commands:

npm init

You can press enter to all of the questions.

npm install --save-dev @types/knockout
npm install -g typescript

Create a tsconfig.json file in the directory as well:

{
}

The contents can be just an empty JSON object for now, this will let the TypeScript compiler use default compilation settings.

General Conversion Semantics

I’ll guide you through the conversion of a typical JavaScript object. We will start with two files:

calculator.js
 
  1. function Calculator()
  2. {
  3.     var _this = this;
  4.     this.valueOne = ko.observable();
  5.     this.valueTwo = ko.observable();
  6.     this.operator = ko.observable();
  7.     this.errorMessage = ko.observable();
  8.     this.result = ko.observable();
  9.     this.calculateClick = function()
  10.     {
  11.         _this.calculate();
  12.     }
  13. }
  14. Calculator.prototype = {
  15.     isValidInput: function()
  16.     {
  17.         if(!this.isValidNumber(this.valueOne()))
  18.         {
  19.             this.errorMessage("Value one is not a valid number!");
  20.             return false;
  21.         }
  22.         if(!this.isValidNumber(this.valueTwo()))
  23.         {
  24.             this.errorMessage("Value two is not a valid number!");
  25.             return false;
  26.         }
  27.         if(!this.isValidOperator(this.operator()))
  28.         {
  29.             this.errorMessage("Operator must be / + - % or *");
  30.             return false;
  31.         }
  32.         return true;
  33.     },
  34.     isValidNumber: function(value)
  35.     {
  36.         if(!value) return false;
  37.         if(value.length <= 0) return false;
  38.         if(!Number(value)) return false;
  39.         return true;
  40.     },
  41.     isValidOperator: function(operator)
  42.     {
  43.         switch(operator) {
  44.             case '+':
  45.             case '-':
  46.             case '%':
  47.             case '/':
  48.             case '*':
  49.                 return true;
  50.             default:
  51.                 return false;
  52.         }
  53.     },
  54.     calculate: function()
  55.     {
  56.         if(!this.isValidInput()) return;
  57.         var valueOne = Number(this.valueOne());
  58.         var valueTwo = Number(this.valueTwo());
  59.         switch(this.operator()) {
  60.             case '+':
  61.                 this.result(valueOne + valueTwo);
  62.                 break;
  63.             case '-':
  64.                 this.result(valueOne - valueTwo);
  65.                 break;
  66.             case '%':
  67.                 this.result(valueOne % valueTwo);
  68.                 break;
  69.             case '/':
  70.                 this.result(valueOne / valueTwo);
  71.                 break;
  72.             case '*':
  73.                 this.result(valueOne * valueTwo);
  74.                 break;
  75.         }
  76.         this.errorMessage("");
  77.         this.valueOne("");
  78.         this.valueTwo("");
  79.         this.operator("");
  80.     }
  81. }
calculate.html
 
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Calculator</title>
  6. </head>
  7. <body>
  8.     <div id="calculator">
  9.         <div>Value One: <input type="text" data-bind="value: valueOne"/></div>
  10.         <div>Operator: <input type="text" data-bind="value: operator"/></div>
  11.         <div>Value Two: <input type="text" data-bind="value: valueTwo" /></div>
  12.         <input type="button" value="Calculate" data-bind="click: calculateClick"/>
  13.         <div data-bind="text: errorMessage"></div>
  14.         <div data-bind="text: result"></div>
  15.     </div>
  16.     <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-debug.js"></script>
  17.     <script src="calculator.js"></script>
  18.     <script>
  19.         ko.applyBindings(new Calculator(), document.getElementById("calculator"))
  20.     </script>
  21. </body>
  22. </html>

 

You can create a file calculator.ts if you would like to follow along.

Replace function constructor with class

First thing we do is rewrite

 
 
  1. function Calculator() 
  2. { 
  3. }

 

With:

 
 
  1. class Calculator
  2. {
  3. }

 

Simple enough.

Give member variables access modifiers and assignments

Both assignment and access modifiers are optional, the access modifier is implicitly public and the value is implicitly unset. Still I like to be verbose, so I make a habit out of always being explicit about access.

We will take this:

 
 
  1. this.valueOne = ko.observable();
  2. this.valueTwo = ko.observable();
  3. this.operator = ko.observable();
  4. this.errorMessage = ko.observable();
  5. this.result = ko.observable();
  6.  

 

And replace it with:

 
 
  1. public valueOne = ko.observable<string>();
  2. public valueTwo = ko.observable<string>();
  3. public operator = ko.observable<string>();
  4. public errorMessage = ko.observable<string>();
  5. public result = ko.observable<number>();

 

Note I have inserted a type into the generics of ko.observable, which allows TypeScript to inference that I have a KnockoutObservable of a certain type, ensuring I can only set my observable to the appropriate type, and ensuring proper type checking when I read and use the observable.

Replace instance methods with arrow methods

Methods on your class that will be used as event handlers, or callbacks of any kind, which need to reference the this context of your object will typically be written this way.

In our case, we need to rewrite calculateClick:

 
 
  1. this.calculateClick = function()
  2. {
  3.     _this.calculate();
  4. }

 

To:

 
 
  1. public calculateClick = () =>
  2. {
  3.     this.calculate();
  4. }

 

I want you to notice two things.  First is the interesting syntax on the method.  This is what TypeScript refers to as an arrow method. Basically, this will ensure the method is an instance method, or, placed in the constructor of the type.

Second, notice the change from our _this to this.  TypeScript will actually rewrite the this references to _this for us, ensuring our this maintains the correct reference to our object for us.

Replace prototype methods with … methods!

Excuse the lame header.  Methods that could be placed in on the prototype of an object can be written how you might expect they would be written.

So taking isValidInput, we can convert this to:

 
 
  1. protected isValidInput()
  2. {
  3.     if(!this.isValidNumber(this.valueOne()))
  4.     {
  5.         this.errorMessage("Value one is not a valid number!");
  6.         return false;
  7.     }
  8.     if(!this.isValidNumber(this.valueTwo()))
  9.     {
  10.         this.errorMessage("Value two is not a valid number!");
  11.         return false;
  12.     }
  13.     if(!this.isValidOperator(this.operator()))
  14.     {
  15.         this.errorMessage("Operator must be / + - % or *");
  16.         return false;
  17.     }
  18.     return true;
  19. }
  20.  

 

Our complete converted file looks like this:

calculator.ts
 
  1. class Calculator
  2. {
  3.     public valueOne = ko.observable<string>();
  4.     public valueTwo = ko.observable<string>();
  5.     public operator = ko.observable<string>();
  6.     public errorMessage = ko.observable<string>();
  7.     public result = ko.observable<number>();
  8.     public calculateClick = () =>
  9.     {
  10.         this.calculate();
  11.     }
  12.     protected isValidInput()
  13.     {
  14.         if(!this.isValidNumber(this.valueOne()))
  15.         {
  16.             this.errorMessage("Value one is not a valid number!");
  17.             return false;
  18.         }
  19.         if(!this.isValidNumber(this.valueTwo()))
  20.         {
  21.             this.errorMessage("Value two is not a valid number!");
  22.             return false;
  23.         }
  24.         if(!this.isValidOperator(this.operator()))
  25.         {
  26.             this.errorMessage("Operator must be / + - % or *");
  27.             return false;
  28.         }
  29.         return true;
  30.     }
  31.     protected isValidNumber(value: string)
  32.     {
  33.         if(!value) return false;
  34.         if(value.trim().length <= 0) return false;
  35.         if(!Number(value)) return false;
  36.         return true;
  37.     }
  38.     protected isValidOperator(operator: string)
  39.     {
  40.         switch(operator.trim()) {
  41.             case '+':
  42.             case '-':
  43.             case '%':
  44.             case '/':
  45.             case '*':
  46.                 return true;
  47.             default:
  48.                 return false;
  49.         }
  50.     }
  51.     protected calculate()
  52.     {
  53.         if(!this.isValidInput()) return;
  54.         var valueOne = Number(this.valueOne());
  55.         var valueTwo = Number(this.valueTwo());
  56.         switch(this.operator()) {
  57.             case '+':
  58.                 this.result(valueOne + valueTwo);
  59.                 break;
  60.             case '-':
  61.                 this.result(valueOne - valueTwo);
  62.                 break;
  63.             case '%':
  64.                 this.result(valueOne % valueTwo);
  65.                 break;
  66.             case '/':
  67.                 this.result(valueOne / valueTwo);
  68.                 break;
  69.             case '*':
  70.                 this.result(valueOne * valueTwo);
  71.                 break;
  72.         }
  73.         this.errorMessage("");
  74.         this.valueOne("");
  75.         this.valueTwo("");
  76.         this.operator("");
  77.     }
  78. }

 

At this point, you may enter tsc to compile the TypeScript, which will produce calculator.js.

Other Tips

All in all, that is a very simple example, but this guide will serve you well and will be all you need to convert the basic semantics of JavaScript to Typescript.  In the real world, you will have external references to other files, so you will need to tell the TypeScript compiler about these types.

Learn To Write Declaration Files

TypeScript allows you to extend types using a d.ts file.  I’d recommend you tell the TypeScript compiler about references you are unable to convert or haven’t converted yet.  You will have to stick to the contract you declare in your d.ts file, which means you will be type safe so long as you don’t break your contract.

Try To Avoid any!

It is a better decision to extend the type in a d.ts file rather than cast to any. If it is a variable that holds multiple types, consider union types. Casts to any will bite you in the butt when you try refactoring, since TypeScript will be unable to refactor the any casted references.

Make a list of files by complexity

 

It is a good idea to get a grip of what files have the fewest external references to other files.  These files are the ideal candidates for initial conversion.  I would suggest skimming through all files and grouping them by those with few external references and those with many.

Leave me feedback if you have any questions or comments!

Leave a Reply

Your email address will not be published. Required fields are marked *