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.
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
i
nstall TypeScript
g
lobally on your machine and allow you to use
tsc
, the
T
ype
S
cript
C
ompiler. 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:
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:
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
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?
Related