Down to the Wire

Zeros and Ones - TypeScript

Hey there! I’m Matthew, and for my inaugural post I’m going to start a hopefully-recurring segment called “Zeros and Ones” - essentially, opinions on a given topic broken down into zeros (negatives) and ones (positives). Today’s post will discuss a language I’ve fallen in love with over the past couple of months: Typescript.

If you’re not familiar with TypeScript, it’s a superset of JavaScript that adds the ability to annotate variables with types. It was created by and is maintained by Microsoft, who introduced it in Ocotober 2012.

Without further ado, let’s dive in!

Installation

Got npm? Great. TypeScript is one step away.

Bash content_copy
$ npm install -g typescript

Although being able to install in a single step from a common package manager is more or less common practice these days, it’s still not something to necessarily be taken for granted. Here, it’s good because it means a very low barrier to entry for getting started.

Type System

As its name would suggest, among TypeScripts’s most useful features is its strong type system. I could probably spend a lot of time discussing why type systems are great, but for now I’ll instead focus on one (somewhat-contrived) maintainability example. Suppose I write some code like this:

JavaScript content_copy
// returns the index of the best choice
function getBestChoice(choices) {
    let best = 0;
    
    for (let i = 1; i < choices.length; i++) {
        if (getChoiceRating(choices[i]) > getChoiceRating(choices[best])) {
            best = i;
        }
    }
    
    return best;
}

// somewhere else
let bestChoiceIndex = getBestChoice(choices);
let bestChoice = choices[bestChoiceIndex];

I later realize that a possible scenario is that all choices have a negative rating, in which case it’s best not to take any of them. Thus, I change the code:

JavaScript content_copy
// returns the index of the best choice
function getBestChoice(choices) {
    let best = 0;
    
    for (let i = 1; i < choices.length; i++) {
        if (getChoiceRating(choices[i]) > getChoiceRating(choices[best])) {
            best = i;
        }
    }
    
    return getChoiceRating(choices[best]) < 0 ? undefined : best;
}

Now, of course, I need to locate all of the times I call getChoiceRating to do an undefined check. This is kind of annoying, and moreover if I miss any I have no indication that I did so except for later breakages. Ugh.

With TypeScript, we instead start with

TypeScript content_copy
// returns the index of the best choice
function getBestChoice(choices): number {
    let best = 0;
    
    for (let i = 1; i < choices.length; i++) {
        if (getChoiceRating(choices[i]) > getChoiceRating(choices[best])) {
            best = i;
        }
    }
    
    return best;
}

On modifying this function, I also change the type declaration:

TypeScript content_copy
// returns the index of the best choice
function getBestChoice(choices): number | void {
    let best = 0;
    
    for (let i = 1; i < choices.length; i++) {
        if (getChoiceRating(choices[i]) > getChoiceRating(choices[best])) {
            best = i;
        }
    }
    
    return getChoiceRating(choices[best]) < 0 ? undefined : best;
}

Now, instead of breaking everything without warning, I get an error if I use it without an explicit cast:

TypeScript content_copy
let bestChoiceIndex = getBestChoice(choices);
let bestChoice = choices[bestChoiceIndex];
// Error: an index argument must be of type 'string', 'number', 'symbol', or 'any'.

So the compiler will tell me everywhere I need to make a fix, which I can do fairly easily:

TypeScript content_copy
let bestChoiceIndex = getBestChoice(choices);

if (bestChoiceIndex === undefined) {
    // do something
} else {
    let bestChoice = choices[bestChoiceIndex];
    // Formerly, this needed to be choices[bestChoiceIndex as number]
    // However, newer versions of tsc understand that the above checker means that undefined isn't possible here!
}

This is just one of many examples of how a type system can help make code easier to maintain. Type systems can also help in development by encouraging authors to write better-quality code as well as making it easier to reason about the code that they write.

Gradual Typing

TypeScript has the concept of an any type, which basically disables typechecking on whatever it’s applied to. The compiler also assumes an implied any type on anything that isn’t explicitly typed, which means that all Javascript is valid TypeScript, and that converting a JavaScript project to TypeScript is as simple as changing the extension. You can then start adding type annotations as you see fit, so there’s basically no overhead to switching a project over. (Though it does get a bit messy when you have dependencies and need their types; more on that later.)

That said, I also like that the compiler has an option to disable the implicit application of any, aptly called noImplicitAny. I highly recommend compiling with this enabled if you’re starting a new TypeScript project from scratch, as it makes sure that everything’s typed (which helps a lot in debugging and error catching).

Alternations (Type Unions)

Another great thing about TypeScript’s type system is the availability of alternations (also called unions), wherein a value can have one of many types. For example, maybe you have a function that returns a number for a valid range of inputs and undefined otherwise - you can type its output as number | void and TypeScript will allow you to do this, whereas many stronly-typed languages would require you to declare the return type as either one or the other, or make a third type that wraps the result.

An even better example of how this can be used is with constant string values, which is something that I never really thought about until I wanted to do it. Let’s say you have a field on an object that can be one of a handful of string values, but shouldn’t legally able to be any other string. For example, perhaps you want to describe a playing card as have a suit attribute that is “spades”, “hearts”, “clubs”, or “diamonds”, and not any other string value. Fortunately, this is pretty easy in TypeScript:

TypeScript content_copy
interface Card {
    suit: "spades" | "hearts" | "clubs" | "diamonds";
    value: number;
}

let twoOfSpadesA: Card = { suit: "spades", value: 2 };

That said, you could alternatively do the same thing with an enum:

TypeScript content_copy
enum Suit {
    Spades,
    Hearts,
    Clubs,
    Diamonds
}

interface Card {
    suit: Suit;
    value: number;
}

let twoOfSpadesB: Card = { suit: Suit.Spades, value: 2 };
// internally represented as { suit: 0, value: 2 }

Each has its advantages: =the string alternation is more human-readable=, but the enum takes up less space internally.

No Restricted Numerical Types

One of the biggest issues with vanilla JS is, in my opinion, the fact that all numbers are in a single generic number type. For a language without the ability to declare a value’s type, that makes sense; however, this is something I expected to see in TypeScript and something that I was surprised and disappointed to not find.

Perhaps the most obvious example is array accesses, since a uint type is exactly what is needed in that case. Take, for example, the following:

TypeScript content_copy
function getElement(grid: number[][], row: number, col: number): number {
    return grid[row][col];
}

Since there are two indexes, we need to make sure we prevent an undefined result on the first index access (which would result in an error on the second). Thus, to make this code safe, we’d need to do something like the following:

TypeScript content_copy
function getElement(grid: number[][], row: number, col: number): number {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || !Number.isInteger(row) || !Number.isInteger(col)) {
        return undefined;
    }
    return grid[row][col];
}

Compare this with how the same function could be handled with int and uint types:

TypeScript content_copy
// int type: good
function getElement(grid: number[][], row: int, col: int): number {
    if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) {
        return undefined;
    }
    return grid[row][col];
}

// uint type: better
function getElement(grid: number[][], row: uint, col: uint): number {
    if (row >= grid.length || col >= grid[0].length) {
        return undefined;
    }
    return grid[row][col];
}

Another use case would be a function like the following:

TypeScript content_copy
let myHeight: uint = 64; // inches
console.log("My height is", myHeight / 12, "feet and", myHeight % 12, "inches");

However, since JS doesn’t do integer division, we end up with My height is 5.3333333333333 feet and 4 inches. Adding functionality to TypeScript to force this into integer division would allow us to not complicate this code (in this case, with flooring).

All in all, it’s not a huge issue in most code I write, but it’s always annoying to have to remember to do checks like isInteger, or remember the efficient ways of truncating.

Occasional Finnicky Behavior

This seems to usually be an issue in scenarios in which some form of static analysis would be required to check for type conformity - and I can’t really blame the authors of TypeScript for not wanting to do that - but it can still lead to some strange type errors nonetheless.

For example, can you figure out why this block emits an error?

TypeScript content_copy
type ActionType = "wait" | "move" | "attack";

const MOVE = "move";

function getMoveString(): ActionType {
	return MOVE;
}

If you need a hint, the emitted error is a type error on the return.

Here’s the answer: MOVE doesn’t conform to ActionType, because it doesn’t match type "move". Confused?

MOVE doesn’t have a declared type, so the TypeScript compiler chooses the one that it thinks is the most applicable: string. This is a reasonable choice. However, this means that getMoveString is returning a string, not "wait" | "move" | "attack".

This particular error might be strange, but not that hard to fix:

TypeScript content_copy
type ActionType = "wait" | "move" | "attack";

const MOVE: ActionType = "move";
// also legal:
// const MOVE: "move" = "move";

function getMoveString(): ActionType {
	return MOVE;
	// Alternatively, don't type MOVE and change this return to
	// return MOVE as "move";
}

While the fixes aren’t so bad, figuring this out can be confusing, especially when your code is thousands of lines rather than just a few. Here’s another example that I’ve run into in the same vein:

TypeScript content_copy
interface Card {
	suit: "spades" | "hearts" | "clubs" | "diamonds";
	value: number;
}

function getSpades(arr: number[]): Card[] {
	return arr.map((val) => ({ suit: "spades", value: val }));
	// Error: return type is { suit: string, value: number }, which doesn't conform to Card
}

ES6 Transpilation

Whatever your favorite ES6 feature is — arrow functions, rest args, destructuring, or something else — we can all probably agree that they are, as a whole, pretty awesome. And while they’re an absolute blast to use, the unfortunate truth is that browser support is incomplete, so nothing in production can really use them.

Or can’t we? With TypeScript, the answer is an emphatic “yes, we can!” TypeScript will automatically transpile ES6 structures down to ES5 so that you can code for the future but have it run in the past. Here’s a neat example of what TypeScript will output when given ES6 (and you can play with it yourself at the TypeScript playground):

To be perfectly fair, if you’re just looking for ES6 transpilation, TypeScript is probably not the way to go; you’d probably be better off with Babel, which has better across-the-board compatibility. That said, most current-version browsers and Node are close to full ES6 support (as of this writing, kangax’s table gives the latest versions of Edge, Firefox, Chrome, and Node at least 90%, with only Safari lagging at 53%), so this will become a less-relevant concern as increasingly-ES6-compatible browsers are adopted.

TypeScript content_copy
function f(...args: number[]) {
    return g(args[0], args.slice(1));
}

function g(mod: number, vals: number[]): number[] {
    return vals.map((v: number) => v % mod);
}

console.log(f(3, 2, 5, 7, 6, 1, 8, 4));
JavaScript content_copy
function f() {
    var args = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        args[_i - 0] = arguments[_i];
    }
    return g(args[0], args.slice(1));
}
function g(mod, vals) {
    return vals.map(function (v) { return v % mod; });
}
console.log(f(3, 2, 5, 7, 6, 1, 8, 4));

Typings Ecosystem

Before I start talking about this, I want to make it clear that I’m not calling the existence of Typings bad; I just think it’s not quite where it needs to be yet. I will, however, mention that it is significantly better than it was when I started using TypeScript (as since then Typings released their 1.0 version, which came with some HUGE systematic improvements). Keep it up, Typings folks!

npm is, in many ways, very magical. It’s wonderful how when you install something:

Bash content_copy
$ npm install --save beautiful-log

…you can then use it immediately:

JavaScript content_copy
var log = require("beautiful-log");

log.log("<blue>It works!</blue>");
log.log("<gray>(Shameless plug is shameless.)</gray>");

…and there’s no extra step to do — it just works right away.

I wish I could say the same for Typings, the equivalent manager for TypeScript definition files. While there are type definitions for plenty of popular Node modules and frontend resources, there’s a whole bunch of extra work to go through before they’re actually usable.

For an example, let’s go through the first steps of making a TypeScript Node project. We can start off by making our directory, initing as needed, and installing the packages we need if we don’t already have them.

Bash content_copy
$ mkdir ts-example
$ cd ts-example
$ typings init
$ npm init -y
$ npm install -g typescript typings
$ npm install --save express
$ mkdir public
$ echo '<h1>It works\!</h1>' > public/index.html

Next, in our favorite text editor, we can make a simple Express static server:

TypeScript content_copy
"use strict";

import * as express     from "express";
import * as http        from "http";

const app: express.Express = express();
const PORT: number = 1337;

app.use(express.static("public"));

const server: http.Server = app.listen(PORT, function() {
    console.info("Listening on *:" + PORT);
});

But then, if we try to build it:

Bash content_copy
$ tsc index.ts
index.ts(3,30): error TS2307: Cannot find module 'express'.
index.ts(4,30): error TS2307: Cannot find module 'http'.

Ok, that makes sense, we haven’t used typings to get the type definitions yet. Let’s try adding the node and express type definitions:

Bash content_copy
$ typings install --save node express
typings ERR! message Unable to find "node" ("npm") in the registry.
typings ERR! message However, we found "node" for 2 other sources: "dt" and "env"
typings ERR! message You can install these using the "source" option.
[...more messages...]

Hmmm… that’s strange. Especially since the modules clearly exist — we can verify that with typings search node and typings search express.

As it turns out, running typings install actually tries to install the entire module, and searches npm for it. Why exactly it does this by default is beyond me. If we only want type definitions, we need to use the --global flag. Additionally, as the second message indicates, we actually need to specify a source for our typings files. In this case, dt, or the DefinitelyTyped repository, is what we want.

Great, let’s try again:

Bash content_copy
$ typings install --save --global --source dt node express
# Another valid (shorter) variation: typings i -SG dt~node dt~express

That succeeded! Now we can try compiling our TypeScript again:

Bash content_copy
$ tsc index.ts
index.ts(3,30): error TS2307: Cannot find module 'express'.
index.ts(4,30): error TS2307: Cannot find module 'http'.

Wait, it still can’t find them?

That’s because now we need to tell TypeScript where to look. If we make a tsconfig.json at the root of our project, we can tell it to additionally search the typings directory for type definitions. It will also automatically build anything it matches, so we don’t need to keep passing it the location of index.ts.

Cool, let’s let tsc handle that for us and then try again:

Bash content_copy
$ tsc --init
$ tsc
index.ts(9,5): error TS2339: Property 'use' does not exist on type 'Express'.
index.ts(11,33): error TS2339: Property 'listen' does not exist on type 'Express'.
typings/browser/ambient/express/index.d.ts(17,34): error TS2307: Cannot find module 'serve-static'.
typings/browser/ambient/express/index.d.ts(18,27): error TS2307: Cannot find module 'express-serve-static-core'.

Great, now we have multiple types of errors, too.

Let’s start with the easiest-to-understand issue first: we’re apparently still missing modules! We seem to also require serve-static and express-serve-static-core, which are both dependencies of the express definition. This is one of the major problems with Typings - when you install a type definition, you don’t get its dependencies as well. This can easily lead to annoying installation chains, installing a defintion, then its dependencies, then their dependencies, and on and on and on.

Let’s fix those missing module problems:

Bash content_copy
$ typings install -SG dt~serve-static dt~express-serve-static-core
$ tsc
typings/globals/serve-static/index.d.ts(13,24): error TS2307: Cannot find module 'mime'.
$ typings install -SG dt~mime
$ tsc

Hey, tsc exits without errors! Looks like the other problems solved themselves when we fixed the missing module issues. We can even run our server and it works!

Bash content_copy
$ node index.js
Listening on *:1337

Note that we’ve now run typings install four times and tsc five times. (And for a first-time user, it’s probably going to be even more than that, seeing as I told you exactly what went wrong on every step.) On the other hand, to install dependencies for this project, we ran npm install exactly one time, and it just worked. It’s impossible to screw up. In contrast, typings is impossible to get correct in one try if you’re totally new to it.

In Conclusion

The score: three zeros, five ones.

Is TypeScript worth it? Totally.

Could it be better? Absolutely.

Will I be using for all of my projects? You can count on it.

Will it replace JS? I doubt it, but that’s not its goal. At most we’d see a type system baked into JS itself - wouldn’t that be exciting? - but I doubt it’s going to surpass vanilla JS as the development language of choice. However, I would like to point out that part of the beauty of TypeScript is that you don’t need to drop your favorite libraries to use it - any library will work with TypeScript, though you may need to do a little bit of extra work if definition files don’t exist for the library already (though for most popular libraries, they will).

All in all, I think TypeScript in its current form is fantastic for development, and definitely a step in the right direction. I look forward to seeing how it improves in the future!