BLOG

JS Static Analysis Part 4 - Using TypeScript

This is Part 4 of a 4 part intro series to static analysis tooling for JavaScript:

TypeScript Documentation

(Note that this post assumes TypeScript 2.x)

Converting to TypeScript

For the final post in this series we’re going to take a look at switching completely over to TypeScript. While TypeScript is a separate language and requires a build step, all existing JavaScript is valid TypeScript as long as you’re writing in the more modern ES6+ format.

Let’s take a look at how you might convert an existing project written in the more traditional ES5 format:

a.js

var b = require('./b');
console.log(b.addTwo(3, 2));

b.js

function addTwo(first, second) {
  return first + second;
}
exports.addTwo = addTwo;

First, let’s take these as-is and run them through the TypeScript compiler. To do that we have two steps:

  1. Rename the files from a.js and b.js to a.ts and b.ts
  2. Change the node/CommonJS require format to the new ES import format

Our new files look like this:

a.ts

import {addTwo} from './b';
console.log(addTwo(3, 2));

b.ts

export function addTwo(first, second) {
  return first + second;
}

Of course if you’re already using ES6+ style JavaScript code, the conversion is even easier. The TypeScript team is trying to match the approved ES spec, only adding on top of it with things like static typing and other features.

Next let’s compile this code:

> npm install -g typescript
> tsc a.ts
> node a.js
5

Here’s what we just did:

  • Install the TypeScript binary as a global npm package (tsc)
  • Point the TypeScript compiler at our entry file (a.ts). This will also find all dependent modules automatically.
  • Run our compiled a.js file.

Adding Type Annotations

This is nice, but just as with flow, TypeScript expects that you’re going to add type annotations to your code.

b.ts

export function addTwo(first: number, second: number) {
  return first + second;
}

Now let’s try to call it:

a.ts

import {addTwo} from './b';
console.log(addTwo(3, 2));
console.log(addTwo('mystring', 2)); // Not Valid

If you’re using an IDE that provides help for TypeScript like Visual Studio Code (or any other popular IDE with a plugin), you’ll get some nice feedback right away:

TypeScript 1

But even without IDE help we get immediate and readable feedback in the compiler:

> tsc a.ts
a.ts(3,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

External Modules

What happens when we want to use 3rd party modules? As of TypeScript 2.0 the recommended way to handle this is to install typings files via npm.

npm install lodash --save
npm install @types/lodash --save-dev

Now when you go to use a library like lodash you get some nice help in your IDE:

TypeScript 2

import * as _ from 'lodash';
_.forEach([1, 2, 3], (i) => {
  console.log(i.length); // This line with throw an error. Numbers do not have a length property
});

And when you compile it, it will provide sensible errors:

> tsc a.ts
a.ts(3,17): error TS2339: Property 'length' does not exist on type 'number'.

Creating Your Own Definition Files

As of October, 2016 the number of type definition files available from the community is much higher than what’s available for flow. However it still won’t cover every package out there. And sometimes a package will get updated before the type definition file is updated.

Let’s start with the simple way. First, let’s create a tsconfig.json file in our root so that we can specify some more advanced options:

tsconfig.json

{
    "compilerOptions": {
        "module": "commonjs"
    },
    "include": [
        "./*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}

Among other things, this now allows us to just run tsc and the compiler will know which files to process. It also knows to output in commonjs format, which is what node.js uses. This is also necessary for IDE support across your entire project.

Next, we’ll use the pad package which doesn’t currently have a typings definition in the registry.

import * as pad from 'pad';
console.log(pad('padme', 10, '-'));

Besides getting an error in our IDE, if we try to compile we get this:

> tsc
a.ts(1,22): error TS2307: Cannot find module 'pad'.

Note, however, that it will still compile for you and output an a.js file. But we want to get rid of this error.

Create a new file called pad.d.ts and just leave it in the root of your project.

declare module 'pad';

It doesn’t actually matter where this file is placed as long as it’s one of the source files that you pass to the TypeScript compiler. We’re doing that here by calling tsc with a tsconfig.json that says to look at all *.ts files in the root of our project.

> tsc
> node a.js
padme-----

Now we don’t get any errors. But at the same time all we’ve done is declared the module. We haven’t enforced any specific type of function definition. It will still accept any parameters.

Let’s see if we can provide more guidance and create a better type definition file. The format expected by this library is the following:

pad(text, length, [options]): Right padding

pad.d.ts

declare module 'pad' {
  function pad(
    text: string,
    length: number,
    options?: any
  ): string;
  namespace pad {}
  export = pad;
}

Here’s how that breaks down, we’ve:

  • Declared a module named pad, similar to our simple example
  • Defined a function (also named pad) that takes 2 required parameters, and 1 optional
  • We’ve said that this function returns a string
  • We’ve exported this as the main function that the module exposes

But actually, if you look at the full definition of the library, there is another supported format for adding left padding by passing the parameters in a different order:

pad(length, text, [options]): Left padding

What can we do now? We just say that it has two possible definitions:

pad.d.ts

declare module 'pad' {
  function pad(
    text: string,
    length: number,
    options?: any
  ): string;
  function pad(
    length: number,
    text: string,
    options?: any
  ): string;
  namespace pad {}
  export = pad;
}

Now we can use both formats in our application:

a.ts

import * as pad from 'pad';
console.log(pad('padme', 10, '-'));
console.log(pad(10, 'padme', '-'));
> tsc
> node a.js
padme-----
-----padme

But actually, that 3rd options parameter isn’t just any. It either needs to be a string like we’re using, or an object specifying more specific options. So let’s do one last modification:

pad.d.ts

interface PadOptions {
  char: string,
  colors: boolean,
  strip: boolean
}
declare module 'pad' {
  function pad(
    text: string,
    length: number,
    options?: PadOptions | string
  ): string;
  function pad(
    length: number,
    text: string,
    options?: PadOptions | string
  ): string;
  namespace pad {}
  export = pad;
}
calendartwitterfeedenvelopelinkedingithub-altbitbucket