TypeScript: The Nitty-Gritty Parts

… at least some of them.

Image by S. Zolkin

There is a myriad of articles about TypeScript out there. Some will tell you that “TypeScript won”. Some will tell you that you don’t need it. Your mileage may vary. That’s a good thing. I don’t believe in absolutes.

I think, in order to decide if TypeScript is for you, you need to see the nitty-gritty parts. Examples from the trenches. Like, how utilizing the power of TypeScript can complement the usage of your favorite library. And of course, you should know about the issues that will certainly cross your path while learning the language.

The following is a transcript of my findings during my on-off relationship with TypeScript. Including my struggles learning the language and the fanboy moments I had over the past two years. I hope that this article will give you a better understanding of what can be achieved with TypeScript and how expressive the language has become.

It’s just JavaScript, right?

TypeScript advocates that it is a “typed superset of JavaScript”. I think this is not quite correct. Others share my opinion. For me, a more accurate description would be:

TypeScript will yell at you if something is potentially unsafe, but even if it is not valid TypeScript, it will still compile down to valid JavaScript.

Of course, the above is only true if noEmitOnError is disabled. You get the point. Still, doesn’t make for a good slogan.

TypeScript has no sound type system. Unlike other languages, runtime errors can still occur. Flags like strictNullChecks, noImplicitAny and noImplicitReturns will make your program safer, but there is no guarantee for soundness. The TypeScript team made this choice to make gradually adopting types easier. Is this worth it? I don’t know.

What I do know is that the more flags you activate, the less it will feel like writing JavaScript. Yes, your system will be more strict, but in exchange TypeScript will get in your way more often. You have to add type declaration all over the place and have to invest a lot of time writing type definitions. This can be very frustrating. Especially if you just started out.

Also, coming from a strongly typed language, you might think that using <> will change the value of a variable. It doesn’t. The <> operator is not a cast. It’s a type assertion, a way to tell the compiler what type it should infer. It is on you to correctly define the type. The compiler will not complain if the type in incorrect. Type assertion can be dangerous. Use it sparingly.

However, there are cases when you have to use type assertion. We used it to patch the API of redux-observable, which is a Redux middleware to handle side effects with RxJS. You can think of it as redux-thunk on steroids.

The later has this nice option of passing additional services to thunks. redux-observables doesn’t support this (yet), so we patched it:

In my experience, it is better to treat the type annotations, quite literally, as annotations. Try to avoid type assertions and play around with the compiler configuration until you find your sweet spot. When it comes to typing, I would suggest the following:

Try to use as little typings as possible for privately held variables (like inside a function) and use as much type annotations as possible for your APIs.

Better Tooling

TypeScript’s homepage promises that

Types enable JavaScript developers to use highly-productive development tools [..]

And this is absolutely true. Exposing the language as a service others can consume enables some powerful tooling. It doesn’t matter if you write TypeScript or regular JavaScript, Visual Studio Code is an awesome editor and “Automatic Type Acquisition” is one of my most beloved features (when it doesn’t DDOS the npm registry). Enabling types without any configuration is so awesome.

At the same time, first-class support for TypeScript is missing in a lot of established tools. This means that you can not use .ts-files as input for these tools. You have to transpile them first.

As a consequence, it can be hard to make tools work well with TypeScript. It usually will at least require some extra work. Work you do not have to do if you use Babel, because (let us be honest) almost everyone else is using Babel and so do the tool authors. Thus, they support it.

Currently, there is no Babel plugin for Typescript. It would make working with tools that already support Babel way easier. Fortunately, the Babel and TypeScript team sat down and talked earlier this year. Let us hope that they will figure something out, so the following story will be a thing of the past.

Some time ago, I had to write a small utility library that generates a unique device identification based on a wide range of parameters. TypeScript shines in these kind of scenarios, because a well documented API would make it much easier for other developers to work with the library. The problems emerged when I wanted to create a coverage report with mocha and istanbul. Long story short. I ended up with the following npm script:

rimraf spec-js && tsc -m commonjs --outDir spec-js --sourceMap --target ES5 -d && node ./node_modules/istanbul/lib/cli.js cover -x 'spec-js/**/*.test.js' -x 'spec-js/*.test.js' _mocha spec-js/**/*.test.js spec-js/*.test.js && remap-istanbul -i ./coverage/coverage.raw.json -o ./coverage/html-report -t html

It took me a while to get there, because I couldn’t figure out how to correctly invoke istanbul and pass the results to remap-istanbu. The later is required to have correct mapping to the TypeScript files.

Here is a rundown of the individual steps:

  1. Delete the spec-js folder, which is basically the cache for transpiled JavaScript test files.
  2. Transpile test files with TypeScript’s CLI and put them in the spec-js folder alongside their corresponding source maps.
  3. Invoke istanbul‘s CLI and tell it to run tests with mocha. The result will be put into ./coverage by default.
  4. Invoke remap-istanbul with the coverage report generated in (3) as input. remap-istanbul will pick up the source maps from (2) automatically and will output an HTML report to ./coverage/html-report.

Get yourself comfortable

TypeScript has its learning curve. Assuming you already know JavaScript, it might not be as difficult as learning a new language. But don’t be fooled. Just because it looks like C#/Java, doesn’t mean it is your cherished enterprise language.

Even though it’s not JavaScript either, you need some understanding how JavaScript works. Otherwise you will have a hard time. Especially in cases where you have to figure out if a problem has something to do with the compiler or the generated JavaScript. Some behavior may look odd to you at first.

For instance, take a look at the code snippets below. Both define a Box class with a readonly property size. The second gist is using the public modifier inside the constructor, which is a shorthand for creating class members. The size argument is optional in both cases.

However, they behave differently. Can you guess why?

Did you figure it out? I didn’t. I had to take a look at the compiled code. Here is the explanation of what is happening:

Calling new Box() without any arguments is possible in both cases, but only in the second example 'medium' serves as a fallback value. In the first one, this.size will be assigned in the constructor no matter what. Declaring it as class property is redundant.

Before Typescript 2.0 there was no protection again this. With the introduction of strictNullChecks, TypeScript will now throw an error, which tells you size is potentially undefined.

Code safe. Enable strictNullChecks.

ES2015 modules and their interoperability with CommonJS (CJS) can also be confusing. It is rather unknown that Babel previously has taken care of issues with the interoperability for developers. TypeScript never did this.

Depending on the compiler configuration, the program would break in the compile step, indicating that there is no default import. Or it broke at runtime, leaving me with a blank page and some errors in the console that could only be traced if the source maps worked correctly.

When it comes to imports remember the following:

Imports like import React from 'react' will try to pick up the default member in the react module. If the module is bundled with CJS this member (usually) doesn’t exist. Thus, you have to use * as React from 'react' in order to obtain the exported object containing createElement, PropTypes and so on.

Sometimes you will see a mix of CJS and the import statement. Even though the outcome will be the same using the * as syntax, import debounce = require('lodash/debounce') looks cleaner. I would suggest doing this whenever the module exports a single function, e.g. module.exports = debounce. Despite the require TypeScript will infer the type of the module.

Falling in Love with Types again

I have to admit that it was bothering me to write all those type annotations at first. I have written large JavaScript apps (>2o0k LOC) and never had an issue with its dynamic nature. Because TDD you know!

What made me finally appreciate all the “extra work”, was returning to a project after 1,5 years. I started to miss the typings. It certainly had something to do with the release of Typescript 2.0 as well, which was a major upgrade (no pun intended).

Control flow based type analysis and tagged unions allow you to write (almost) type safe reducers:

The things you can do with the typeof operator is amazing. If you import an module with * as utils from 'my-utils', typeof utils lets you use the utils variable as a type. One less interface to define!

Side note: If you want to create a tagged union make sure you’re using const rather than let. TypeScript has this concept of widening string literals. Because let can be reassigned the value is widened to a string.

With TypeScript 2.1 keyof and mapped types where introduced, which made the type system even more powerful. While we have to wait a little longer until 3rd-party library typings support these features, you can already benefit from them.

The snippet below shows how you can create getters that have “type safe” string parameters. Using mapped types (T[K]) TypeScript can infer the return type from those getters. In the example only primitives are used, but it works with complex types too!

That’s it for now

Initially TypeScript can feel like this half-baked language. Because enterprise developers think they finally got their strong, statically typed language to code web applications. JavaScript developers perceive the type annotations as an obstacle with no benefits. Both end up dissatisfied.

I believe the team behind TypeScript created something unique. Something that is worth exploring. The latest releases really convinced me to invest in the language. TypeScript got much more complete and it can now deal really well with the dynamic nature of JavaScript. It’s really amazing how a few type annotations can make a big difference to the static verification of your code.

If you ask me, a lot of the confusion around TypeScript is caused because of people think it’s “just JavaScript” or enterprise JavaScript (whatever this means). It’s not. Get over it. It’s a language of its own (with JavaScript interoperability).

Tool authors started to invest time and resources to improve TypeScript support. Libraries with first-class TypeScript support are popping up more and more.

I hopefully contributed a little bit to the TypeScript community and convinced one or two people to give it a spin.

Thanks for reading 💙