How to migrate from JavaScript to TypeScript?

After the previous posts about TypeScript, I'm sure you'd like to migrate your application from JavaScript to TypeScript. If you haven't read them yet, take a few minutes:

The migration from JavaScript to TypeScript is not a very complicated process. But there are some steps to follow to achieve it correctly without spending too much time.

Learn the TypeScript language

Of course, you have to start learning the language. If you are already a JavaScript developer, learning TypeScript is easy.

Here's a few good links:

You'll also find useful resources on this blog 😃

Install TypeScript

If you are using Visual Studio, the TypeScript compiler is already installed. In other cases, you must install it.

npm install typescript
.\node_modules\.bin\tsc --version

or you can install it globally

npm install -g typescript
tsc --version

Add the TypeScript configuration file to your project

The configuration file indicates to the TypeScript compiler the options to use to compile the project. This is also the root of the TypeScript project.

Your solution should look like:

Root
├── src
│   ├── file1.js
│   └── file2.js
├── built
└── tsconfig.json

The tsconfig.json at the root of the project contains:

{
  "compilerOptions": {
    "target": "es5",
    "allowJs": true,
    "outDir": "./built"
  },
  "include": [
      "./src/**/*"
  ]
}

The file indicates the TypeScript compiler to accept JavaScript files, so you don't need to change all your code at once. With this option, you can convert your files one by one. The outDir indicates where TypeScript outputs the files after the compilation. The include options indicates where are located the source files.

By default, the compiler won't analyse the error in the JS files. You can start getting some of the bemefits of TypeScript by indicates the compiler to check your JS files using "checkJs": true:

{
  "compilerOptions": {
    "target": "es5",
    "allowJs": true,
    "checkJs": true,
    "outDir": "./built"
  },
  "include": [
      "./src/**/*"
  ]
}

The compiler will use the information it can gather from your files to indicate errors (documentation). For instance, it can use JSDoc to find types or resolve require("...") statements. So, You'll get some of the TypeScript advantages immediately. You can also set some compiler options such as noImplicitReturns: true, noFallthroughCasesInSwitch: false, allowUnreachableCode: false or allowUnusedLabels: false.

/** @type {string} */
var author;

author = "meziantou"; // OK
author = false;       // Error: boolean is not assignable to string

Convert files to TypeScript and fix common errors

It's time to start using TypeScript. The only change required is to change the extension a the files from .js to .ts. You don't need to migrate all files at once. You can migrate files one by one. If you have a large code base, you'll probably get hundreds of errors. So, converting files one by one will allow to handle them more easily.

Some of valid JavaScript will stop working in TypeScript. Mainly because of the type checker. Here's a few errors you may encounter.

Sequentially Added Properties

The following code is very common in JavaScript:

var author = {};
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

In TypeScript the type of author is an empty object. So nickname and fullname doesn't exist and are not assignable. The solution is to move the declarations into the object creation:

var author = {
    nickname = "Meziantou",
    fullName = "Gérald Barré"
};

The other solution is to create a type for the author variable, so the compiler knows the list of properties available.

interface Author { nickname: string; fullName: string }

var author = {} as Author;
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

The latest option, which is not the best, is to indicate the compiler to not check the usages of the author variable using the type any:

var author: any = {};
author.nickname = "Meziantou";
author.fullName = "Gérald Barré";

Using a well-know library

TypeScript must know the types of every objects to compile a file. So, if you are using a library such as JQuery, loadash, etc., you have to tell TypeScript what the library contains. Fortunately, the TypeScript definitions of thousands of lib are available via npm. For instance, if you use jquery, you can install the definition using

npm install @types/jquery

You'll find the complete list of supported libraries on npm: https://www.npmjs.com/~types

Using others libraries

If the types are not available, you have 2 options:

  • Create the definition of the methods/objects
  • Create a dummy declaration

For instance, you use a lib that exposes a function add(a, b) and an object _, you can create the following dummy declaration, so the compiler is happy. The any type indicates to the compiler to not check the value.

declare function add(...args: any[]): any;
declare var _: any;

add(1, 2);
_.filter(characters, { 'age': 36 });

With these definitions, anything after _ will be valid. While this is not the philosophy of TypeScript, it's better to have 80% of the code checked instead of 0%. So, it's a good start. Later, you can write a better definition such as:

declare function add(a: number, b: number): number;
declare var _: Underscore;

interface Underscore {
    filter(obj: any[], filter: { [name: string]: any });
}

External modules (require, define)

If you are using modules with commonjs, requirejs or amd, you can use a dummy declaration:

declare function require(path: string): any;
declare function define(...args: any[]): any;

However, you should use the TypeScript syntax. This is more convenient and you'll get the typing information. First, change the module option in the tsconfig.json file:

{
  "compilerOptions": {
    "module": "commonjs" // Set the kind of module you are using
  }
}

Then change your import statements:

// JavaScript
var math = require("./math");

// TypeScript (one of the following)
import math = require("./math");
math.add(1, 2);

import { add } from "./math";
add(1, 2);

You can also replace the exports with the new syntax

// JavaScript
module.exports.add = function(a, b) {
    return a + b;
}

// TypeScript
export function add(a, b) {
    return a + b;
}

Use new syntaxes

You know have a valid TypeScript project. You can start using the new TypeScript language features. This should help you writing more readable code. For instance, you can:

  • Replace immediately-invoked function expressions with namespace/module
  • Use classes when possible
  • Replace var with let or const
  • Replace magic numbers/strings with enumeration
  • Replace require() with import (check the previous section)
  • Use async await to simplify the usage of asynchronous code
  • Replace property names with nameof equivalent

Some tools such as Resharper allows to replace common JS syntax with a TypeScript equivalent: https://blog.jetbrains.com/dotnet/2015/02/05/ways-and-advantages-of-migrating-javascript-code-to-typescript/

Enable compiler checks to find more errors

Finally, you should change the compiler options to enable more and more checks. This will help you find more errors at compile time. Some of them such as strictNullChecks will generates lots of errors (maybe thousands). So, you should enable these option one by one and fix the new errors.

{
    "compilerOptions": {
        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "alwaysStrict": true,
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noFallthroughCasesInSwitch": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "suppressExcessPropertyErrors": false,
        "suppressImplicitAnyIndexErrors": false,
        "strictFunctionTypes": true,
        "strictNullChecks": true,
        "strictPropertyInitialization": true,
        "checkJs": true // If you are still using some js files
    }
}

You can read more about all these options in a previous post: Detect common JavaScript errors with TypeScript.

Leave a reply