/ typescript

Moving from JavaScript to TypeScript

I won't lie, I love TypeScript. Microsoft, say about them what you wish, but they do know how to support their developers ("Developers! Developers! Developers").

Why would you use TypeScript over pure JavaScript? It requires compilation. No native browser support. What are the benefits?

Type safety. If you have worked with languages such as Java and C++ you will know the advantages of having types specified. Depending on your IDE (I prefer Visual Studio Code, but IntelliJ has great support, as does the full Visual Studio), you will even pick up mistakes before compilation.

If you have used Babel to compile ES6 code to ES5, then you're in a good place to move. If not, you're in for a treat. So much in the way of cool stuff has been added to ECMAScript 6 that is available to TypeScript developers (things like classes, interfaces, imports, arrow functions, etc, most of which are beyond the scope of this article, but so very useful).

To start with, I'm going to assume you have Node.js and NPM installed (a pretty standard assumption for a JavaScript developer).

So go to your project (assuming you already have your package.json set up - if not, a basic npm init -y will be good enough), and start with

npm install typescript -g
npm install typescript --save-dev

Essentially we're adding the TypeScript compiler to be global, then we're adding it as a development dependency to our project (final output is standard JavaScript so no need as a run time dependency). Basic stuff.

Next we generate a tsconfig.json file as a starting point.

tsc --init

Open up this file and you'll see some basic settings shown, and a whole heap commented out. Lets grab what we need (I've removed all the commented out lines just to keep this easy to read). This is just a starting point, I'll be adding more options as we go through.

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "sourceMap": true,                     /* Generates corresponding '.map' file. */
    "outDir": "./dist",                        /* Redirect output structure to the directory. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "alwaysStrict": true                      /* Parse in strict mode and emit "use strict" for each source file. */
  }
}

Ok, so the options here have some nice comments, but I'll explain a little further.

  • target = es5 A good starting point to run in the browser. If you're just writing backend scripts for Node.js, use es6 here so that we don't have to compile down to as messy code
  • module = commonjs To be honest, I'm not sure of the advantages and disadvantages here. Commonjs just works, so I leave it.
  • sourceMap = true This is for debugging purposes, feel free to change it when releasing to production. But Chrome is amazing at picking up the files that the js was built from for setting breakpoints etc.
  • outDir = ./dist Feel free to set this to any folder you wish, but I like dist as a standard location. This is where your compiled JS files end up.
  • strict = true Strict type checking. If you're using TypeScript in general, you want this
  • alwaysStrict = true Just adds the standard 'use strict' at the top of the JavaScript source files.

Next step is simple. Rename your *.js files to *.ts. TypeScript is a superset of JavaScript, so with the right tsconfig.json settings, it should just compile and work, but now you have the ability to optionally add types to your variables.

Setting up compilation is reasonably easy as there's plugins for most tools. For Webpack, awesome-typescript-loader does exactly what it's name says. To use it in your webpack.config.js file, add/update the following

module: {
    rules: [
        { test: /\.ts$/, loader: "awesome-typescript-loader" }
    ]
},

If you're using gulp, it's only slightly more complex, you'll need to install gulp-typescript and add/update the following to Gulpfile.js

// Pull in the TypeScript Config
const tsProject = ts.createProject('tsconfig.json');

gulp.task('scripts', () => {
    const tsResult = tsProject.src()
        .pipe(tsProject());
    return tsResult.js.pipe(gulp.dest('dist'));
});

Grunt is similar, using grunt-ts, update your Gruntfile.js as follows

grunt.initConfig({
    ts: {
        default: {
            src: ["**/*.ts", "!node_modules/**"]
        }
    }
    // Rest of your grunt init config
});
grunt.loadNpmTasks("grunt-ts");
grunt.registerTask("default", ["ts"]);

If you just want to use the command line, just type

tsc

For testing, check out the ts-node library, it will let your code run without compilation. I'll leave that to you to find however.

So if you're slowly transitioning (for example, just adding types to new code), you can stop here, however to do this properly, lets continue.

Any libraries that you are using, you'll need to grab type definitions for. This is usually a case of

npm install @types/<library> --save-dev

Some libraries will already include TypeScript definitions so this isn't required for those (you'll usually get a warning when you try to install a type definition for these).

The next step is to add the compiler flags

"noImplicitAny": true,
"noImplicitReturns": true

to your tsconfig.json file. This will generate a whole lot of errors in your IDE/compilation.

So the easiest fix is to go through and add the any type to all your variables and methods. And making sure all paths of a function return a value properly Eg.

// Old code
var test;
function testMethod(arg) { ... }
// New code
var test: any;
function testMethod(arg: any): any { ... }

This is a good initial pass, but even better is to add the proper types where easy, Eg.

var test: string;

If it's not simple to do that (whether it's hard to work out the type it should be, or it's coming from libraries), then just leave it as any for now, and fix that up on the next pass through your code.

Much like other modern languages, generics are available in TypeScript also, so for an array type, you'd look at something like

let myArr: Array<string>

Make a note of the let keyword rather than var. You should move to this also as it makes your variable function in a sane way. Also check out const for anything that doesn't change (as a side note, if you're following functional programming, most things will be const rather than let or var). These are ES6 features and not exclusive to TypeScript.

You should be able to compile and run it from here. You may have even picked up a few bugs while looking through and refactoring your code (this is the beauty of TypeScript over JavaScript)

Next up are optional parameters. You may have even come across this while adding your any types. This is extremely simple (and actually a shortcut which I'll explain below). It's just a matter of adding a ? to your variable.

// Old code
function testMethod(optionalArg) { ... }

// New code
function testMethod(optionArg?: any): any { ... }

TypeScript is usually pretty good at working out if you have done null/undefined checks before using a variable, but if for some reason it isn't playing nice, add a ! to the end of your variable to indicate that it's fully defined and not null at this point

someObject!.func('blah');

You can define any custom types you may require which is extremely handy, so for example

type Person = {
    name: string,
    age: number
}
let user: Person;

This is often the best next step through your code. But you may wish to combine with the following.

This is some of the true power of TypeScript and one of my favourite features, compound types. The ability to combine types, or set a list of types that a variable could be is amazing. Especially for return values.

Imagine you have a function that you want to say could return a string or number, but not a null (ie. will always be valid), try this.

function someFunc(inVal: string): string | number { ... }

Whenever you call this, your return will always be valid, no need for null/undefined checks.

Or how about the opposite, a call that could potentially return null

function someFunc(inVal: string): string | null { ... }

In fact, adding a ? to an optional parameter is equivalent to the following

function someFunc(inVal: string | undefined): string { ... }

The other very cool thing is appending types. Eg.

type Blah {
    blah: string,
    bob?: number
}

type BlahActions {
    someFunc: Function
}

class DoBlah {
    constructor(blahObj: Blah & BlahActions) { ... }
}

So this is saying that blahObj is an object that has both the Blah types, and the BlahAction functions in it. The beauty is that it gets deconstructed at compile time, so that if from the call here the object looks something like

type ExternalBlah {
    blah: string,
    bob?: number,
    someFunc: Function
}

and pass that to DoBlah, it's perfectly valid. This is amazing when it comes to using libraries such as React and Redux allowing you to split your code and make it more readable and testable.

One very cool feature is you can restrict a type to a set of string literals. For example

type Name = "Bob" | "Sam" | "Joe";

So if you try to assign a different value to a variable with type Name, you'll get an error. Just remember though, as with all TypeScript, this is only a compile time check, any user input from your interface don't have to follow these rules.

Think about this in the context of methods also

function getValue(): number | "default" { ... }

So once you start doing this, you'll definitely want to enable the following in your tsconfig.json file

"strictNullChecks": true

Types and Interface definitions are fairly interchangable in TypeScript, however I definitely prefer types when it's just an object, and interfaces when a class has to implement something.

Right here is a good place to stop and call your conversion done. However to take it to the next level, continue.

So time to modernise your libraries. Using var blah = require('blah'); is really the old way of doing things, sometimes libraries still need this, but generally you want to do something along the lines of

import * as blah from 'blah';

or if using something specific

import { Blah } from 'blah';

or when using defaults

import Blah from 'blah';

However if you're using libraries that don't have type definitions available you'll have to add the following:-

Create a file to do it for you following the format <library>.d.ts with the following inside declare module '<library>';.

Or the other alternative is to add the following to your tsconfig.json

"suppressImplicitAnyIndexErrors": true

Try to avoid this wherever possible though as you'll lose type safety when dealing with these libraries.

Look for type safe versions of libraries you may already use. For example, Facebook's immutable library has a Record class to take place of Map which will let you design precisely how you want your data structure to look.

One other neat thing about TypeScript is an experimental feature from ES7. Decorators. There are already a few libraries out there using them, I'm a big fan of mocha-typescript which makes your testing code look a lot more like nUnit type tests. For example

import { expect } from 'chai';
import { suite, test } from 'mocha-typescript'

@suite('Group for testing example')
class TestExample {
    @test('It tests Hello World!')
    helloWorld() {
        const hello = 'Hello World!'; // No need for a type here as it's implied by the assignment
        
        expect(hello).to.equal('Hello World!');
    }
}

You'll need to add

"experimentalDecorators": true

to your tsconfig.json to enable this

So much when first jumping to a type safe language from a type unsafe one, you'll get frustrated with errors while you code, jumping back and forth fixing and updating types, but at the end of the day, remember that this reduces the amount of bugs in production code, and makes your code a lot more understandable in the first place.