TypeScript: The Nitty-Gritty Parts
… at least some of them.
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.
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
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.
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.
TypeScript’s homepage promises that
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
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:
- Delete the
- Transpile test files with TypeScript’s CLI and put them in the
spec-jsfolder alongside their corresponding source maps.
istanbul‘s CLI and tell it to run tests with
mocha. The result will be put into
remap-istanbulwith the coverage report generated in (3) as input.
remap-istanbulwill pick up the source maps from (2) automatically and will output an HTML report to
Get yourself comfortable
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:
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
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:
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
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
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).
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
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