In this article, we’ll discuss TypeScript, its benefits, and how to introduce it to a legacy piece of JavaScript code.

By the end this article you’ll learn:

  • What TypeScript is and what its benefits and tradeoffs are
  • How to get started with TypeScript with a legacy JavaScript codebase
  • How to use type annotations in TypeScript
  • How to use nullability checks in TypeScript
  • Next steps for further improving TypeScript code

What is TypeScript?

So, what is TypeScript and why should you use it?

Put simply, TypeScript is a superset of JavaScript. Think of it as JavaScript with additional annotations and static type checking.

TypeScript transpiles down to JavaScript, so any browser that runs JavaScript can run code written in TypeScript. TypeScript can also target older versions of JavaScript. This lets you use modern JavaScript features like classes, arrow functions, let/const, and template strings while targeting browsers that don’t yet support these things.

Additionally, TypeScript’s static checking makes entire classes of defects impossible, which is something I feel very strongly about.

With that brief introduction, let’s meet the app we’ll be migrating to TypeScript.

The Sample Application

We’ll be working with a simple JavaScript application that we’ll migrate to TypeScript.

The code is available on GitHub in its initial JavaScript state (with a few bugs) and its finished TypeScript state. If you’d like to play with the final fixed version in your browser, it is available online.

The app is a simple test case manager where the user types in the name of a test case and adds it to the list. Test cases can then be marked as passed, failed, or deleted.

The sample application, showing a list of test cases

This is an intentionally simple and intentionally buggy app. It doesn’t use any JavaScript frameworks or even any JavaScript libraries – not even JQuery or Underscore / Lodash.

The app does use Bootstrap v4 with Bootswatch’s Darkly theme in order to keep the HTML simple with a clean UI for this article.

Existing HTML

While our focus is going to be on the JavaScript, there are a few things in the HTML to be aware of:

Specifically, let’s look at a few lines:

  • Line 7 imports our main JavaScript code
  • Line 22 references addTestCase defined in our JavaScript code.
  • Line 27 – lblNoTestCases is a label that is shown if no test cases exist
  • Line 28 – listTestCases is a placeholder for the test case UI elements

Startup JavaScript Code

With that aside, let’s look at the existing code in a few chunks:

Here we define a TestCase class that serves as our primary (and only) entity in this application. We have a collection of testCases defined in line 1 that holds the current state. At line 20, we add a startup event handler that generates the initial application data and calls out to the function to update the test cases.

Pretty simple, though it does contain at least one bug (see if you can find it before I point it out later).

Rendering JavaScript Code

Now, let’s look at our list rendering code. It’s not pretty since we’re not using a templating engine or a fancy single page application framework like Angular, Vue, or React.

The code here is relatively self-explanatory and clears out the list of items, then adds each item to the list. I never said it was efficient, but it works for a demo.

Like the last, this chunk contains at least one bug.

Event Handling JavaScript Code

The final chunk of code handles events from the user.

This specifically handles button clicks and adding items to the list.

And, again, there’s at least one bug in this chunk.

What’s Wrong with the Code?

So, what’s wrong here? Well, I’ve observed the following problems:

  • It’s impossible to fail or delete the initial test data.
  • It’s impossible to fail any added test
  • If you could delete all items, the add item label wouldn’t show up

Where the bugs are isn’t the point. The point is: each one of these bugs would have been caught by TypeScript.

So, with that introduction, let’s start converting this to TypeScript. In the process, we’ll be forced to fix each one of these defects and wind up with code that can’t break in the same way again.

Installing TypeScript

If you have not already installed TypeScript, you will need to install Node Package Manager (NPM) before you get started. I recommend installing the Long Term Support (LTS) version, but your needs may be different.

Once NPM is installed, go to your command line and execute the following command: npm i -g typescript

This will install TypeScript globally on your machine and allow you to use tsc, the TypeScript Compiler. As you can see, although the term for converting TypeScript code to JavaScript is transpiling, people tend to say compiler and compilation. Just be aware that you may see it either way – including in this article.

With this complete, you now have everything you need in order to work with TypeScript. You don’t need a specific editor to work with TypeScript, so use whatever you like. I prefer to work with WebStorm when working with TypeScript code, but VS Code is a very popular (and free) alternative.

Next, we’ll get set up with using TypeScript in our project.

Compiling our Project as a TypeScript Project

Initializing TypeScript

Open a command line and navigate into your project directory, then run the following:

tsc --init

You should get a message indicating that tsconfig.json was created.

You can open up the file and take a look if you want. Most of this file is commented out, but I actually love that. TypeScript gives you a good configuration file that tells you all the things you can add in or customize.

Now, if you navigate up to the project directory and run tsc you should see TypeScript displaying a number of errors related to your file:

These issues are all valid concerns, but for the moment, let’s disable some by editing the tsconfig.json file and setting "strict": false,.

Now, if you try to compile, you’ll get a much smaller subset of errors. Most of them seem to be around the TestCase class, so let’s take a look at that now.

Type Annotations

Most of the errors seem to be around isPassing and id not being defined on that class. That makes sense since we were using JavaScript’s innate ability to dynamically define properties. Since we’re using TypeScript’s checking, we’ll need to define those fields now:

Lines 8-10 are new here and define the missing fields. Note that we have type annotation syntax here in the : string, : boolean, and : number definitions.

Type Assertions

Next, we’ll address an issue in the addTestCase method. Here, TypeScript is complaining that HTMLElement doesn’t have a value field. True, but the actual element we’re pulling is a text box, which shows up as an HTMLInputElement. Because of this, we can add a type assertion to tell the compiler that the element is a more specific type.

The modified code looks like this:

const textBox = <HTMLInputElement>document.getElementById('txtTestName');

Important Note: TypeScript’s checks are at compile time, not in the actual runtime code. The concept here is to identify bugs at compile time and leave the runtime code unmodified.

Correcting Bad Code

TSC also is complaining about some of our for loops, since we were cheating a little and omitting var syntax for these loops. TypeScript won’t let us cheat anymore, so let’s fix those in updateTestCases and findTestCaseById by putting a const statement in front of the declaration like so:

function findTestCaseById(id) {
    for (const testcase of this.testCases) {
        if (testcase.id === id) return testcase;
    }

    return null;
}

Fixing the Bugs

Now, by my count, there are two more compilation issues to take care of. Both of these are related to bugs I listed earlier with our JavaScript code. TypeScript won’t allow us to get away with this, thankfully, so let’s get those sorted out.

First of all, we call to showAddItemsPrompt in updateTestCases, but our method is called showAddItemPrompt. This is an obvious issue, and one that could conceivably be caused either by a typo or renaming an existing method but missing a reference. This is easily changed by making sure the names match.

Secondly, failTestCase declares a variable called testCase and then tries to reference it as testcase, which is just never going to work. This is an easy fix where we can make sure the names are consistent.

Referencing our Compiled Code

And, with that, running tsc results in no output – that mean our code compiled without issue!

On top of that, because Logic.ts will automatically transpile into Logic.js, the file our index.html is referencing anyway, that means that we don’t even have to update our HTML.

And so, if we run the application, we can see that we can fail and delete tests again:

An animated gif showing that failing, passing, and deleting new items works

But wait, weren’t there three errors in the code? TypeScript only found two!

Well, yes, but we haven’t told TypeScript enough to find the third one yet. Let’s fix that by re-enabling strict mode.

Strict Mode

Going back into tsconfig.json, set strict to true.

This should yield about 16 errors during compile. The vast majority are no impicit any, or TypeScript complaining that it doesn’t know what type things are. Going through and fixing that is somewhat straightforward so I won’t walk through it, but feel free to check my finished result if you get lost.

Beyond that, we see a few instances where TypeScript points out that things could be null. These involve fetching HTML Elements from the page and can be resolved via type assertions:

const list = <HTMLElement>document.getElementById('listTestCases');

The type assertions are acceptable here because we are explicitly choosing to accept the risk of a HTML element’s ID changing causing errors instead of trying to somehow make the app function without required user interface elements. In some cases, the correct choice will be to do a null check, but the extra complexity wasn’t worth it in a case where failing early is likely better for maintainability.

Removing Global State

This leaves us with 5 remaining errors, all of the same type:

'this' implicitly has type 'any' because it does not have a type annotation.

TypeScript is letting us know that it is not amused by our use of this to refer to items in the global scope. In order to fix this (no pun intended), I’m going to wrap our state management logic into a new class:

This generates a number of compiler errors as things now need to refer to methods on the testManager instance or pass in a testManager to other members.

This also exposes a few new problems, including that bug I’ve alluded to a few times.

Specifically, when we create the test data in buildInitialData we’re setting the id to '1' instead of 1. To be more explicit, id is a string and not a number, meaning it will fail any === check (though == checks will pass still). Changing the property initializer to use the number fixes the problem.

Note: This problem also would have been caught without extracting a class if we had declared type assertions around the testcases array earlier.

The remaining errors all have to do with handling the results of findTestCaseById which can return either a TestCase or null in it’s current form.

In TypeScript, this return type can be written explicitly as TestCase | null. We could handle this by throwing an exception instead of returning null if no test case was found, but instead we should probably heed TypeScript’s advice and add null checks.

I’ve glossed over many details, but if you’re confused on something or want to see the final code, it is available in my GitHub repository.

Benefiting from TypeScript

Now, when we run the application, the code works perfectly

The JavaScript app after being migrated to TypeScript. All functionality works as expected.

Not only that, the compiler itself makes sure that the errors we encountered will never be possible again (if we keep playing by the rules, anyway).

Additionally, TypeScript helped us gracefully handle potential errors down the road by forcing us to think about potentially null values.

Next Steps

If you’re interested in getting more in-depth with TypeScript, stay tuned as I intend to cover more topics of note including:

  • Linting to find additional issues
  • Testing TypeScript with Jest
  • Automatically formatting code with Prettier
  • Bundling files together
  • Using NPM and WebPack to manage complex build processes

If you’d like to start with a fresh project already set up for these things, I recommend you check out Christoffer Noring’s TypeScript Playground repository on GitHub.

Closing Thoughts

There’s been a recent surge of people attacking TypeScript for getting in the way, obfuscating JavaScript, being unnecessary, etc. And sure, maybe TypeScript is overkill for an app of this size, but here’s where I stand on things:

TypeScript is essentially a giant safety net you can use when building JavaScript code. Yes, there’s effort in setting up that safety net, and no, you probably don’t need it for trivial things, but if you’re working on a large project without sufficient test coverage, you need some form of safety net or you’re going to be passing off quality issues to your users.

To my eyes, TypeScript is an incredibly valuable safety net that supports existing and future unit tests and allows QA to focus on business logic errors and usability instead of programming mistakes.

I’ve taken a large JavaScript application and migrated it to TypeScript before to great effect. In the process, I resolved roughly 10 – 20 open bug tickets because TypeScript made the errors glaringly obvious and impossible to ignore.

Even better, this process made the types of errors that had occurred anytime the app was touched impossible to recur.

So, the question is this: What’s your safety net? Are you really willing to let language preferences pass on defects you might miss to your end users?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.