From callback swamp to typescript heaven: why we rewrote our entire api in typescript

Written by aherve | Published 2016/12/05
Tech Story Tags: javascript | nodejs | typescript | es6 | api

TLDRvia the TL;DR App

Theseus also went back from (callback) hell

At Hunteed we recently rewrote our entire api codebase, migrating from a standard node-express api to a brand-new typescript app. This was quite a challenge, and it took quite some time so the choice of doing so was not obvious to make at the time.

Now that we’re done with it, we think the benefits of doing so are tremendous. Here’s why.

TL;DR

  • It’s more robust
  • It’s easier, faster to understand/maintain
  • It’s 25% fewer lines of code
  • No more callback-hell
  • Typescript learning curve: very low

What we started with (a.k.a. why bother ?)

We started with a node-express javascript app, originally bootstraped with a popular yeoman build.

The code was es6-written, and some perplexing babel config was somehow making things work, though I wasn’t so sure why and how. We also ignored the big red flag saying that babel should not be used in production (but was it really?).

The app code coverage was good. However, testing new features required a lot of boilerplate and intricated callbacks before you could actually write a test. This was a real trouble, as the temptation to write untested code only would only grow.

I’ve come to think that using a lot of mongoose hooks made the code a bit difficult to understand. First it looked nice and efficient, but at some point it was not obvious to see what happened when you called a method. Things could be more explicit.

A lot of callbacks. Callbacks everywhere. It did not felt like such an issue at the time, as we got quite accustomed to use them. You’ll see how we changed our minds about that :)

Why typescript ?

1. Obvious advantages of typings

Typing your code adds a lot of robustness to your project. This has been discussed a lot elsewhere, but some main advantages could still be mentioned here:

  • Consistency: Some people say if you have functions that take many arguments, then at some point, someone will reverse them. Calling f(foo, bar) instead of f(bar, foo is now impossible. Typescript checks the input/outputs so you won’t commit such a mistake.
  • Null error checking: in typescript2, you can check null errors at compile time:

  • I/O types checking: The compiler knows what the input/output types of your functions are.

Note that [1, 2, 3, 4].map(x => add(x, 1)) will produce an array of numbers [2, 3, 4, 5]. The compiler is able to figure it out by itself.

2. Await is the new callback

One of the typescript features I like the most is the ES7-like async/await. Although this is currently an ES7 proposal, typescript already supports it, and it knows how to transpile await to ES6 code (by taking advantage of the ES6 iterators, but you don’t have to worry about that as a dev).

[edit]: since typescript 2.1, you can transpile [_await_](http://react-etc.net/entry/async-await-support-for-es5-browsers-pushed-to-typescript-2-1) to es2015 \o/

This post covers the details about async/await. Long story short, as long as a function is declared as async, it will return a Promise. Moreover, it will allow to use await, wich tells the compiler to wait for a promise to resolve, without blocking the thread. For instance,

This is non blocking, although it looks like regular synchronous code we all dream of

This allowed us to change some code that looked like

our old, tedious, cb syntax

to the now fancy

new await syntax

ConclusionGetting rid of the tedious callback/callback error handling process allowed us a reduction of 25% of the codebase !

Want to “promisify” a function of yours ? easy as pie:

3. Fancy tests

We used to have a lot of callbacks/promises in our tests. As a result, it became more and more difficult to even properly initialize a test.

Having a testing environment that is tedious to work with is a catastrophic thing. At some point, someone will skip testing, because it is too much of a hassle.

With the awesome supertest-as-promised lib and async functions, testing a route has now become quite elegant:

Look ma, no callbacks !

Yeaaah, but how do I deploy ?

Same as usual, except that you should transpile your code before uploading it. If you choose to use await , be aware that you need to transpile to at least ES6. So you will need to run a recent enough version of nodejs. Most of the modern tools do (like heroku, modulus…), and it should not be an issue for backend-api deployment.

At Hunteed we open-sourced a build (checkout this story for more details) that you can use. The workflow can be summarized as:

  • write typescript code
  • saving file triggers a code transpilation that outputs new js in /dist
  • changing the js files triggers a test run. Note that the tests are actually run against the transpiled javascript files. Which is good, since the transpiled javascript will actually be run in prod. Of course, map files allow to link the errors to your typescript files.
  • Once your dist directory is transpiled and tested, it is simply a good old ES6 node-express app, that you can safely send to prod. Obviously this is a CI-server work. You could, but you won’t do that from your computer.

How about the learning curve ?

In my opinion, getting to code in typescript takes less than a day for an experienced javascript developer. Changing your function (user) {...} to function (user: User) {...} is already enough to benefit from the ts compiler awesomeness. If you feel brave enough, then you can turn on the noImplicitAny compiler options, that will ensure that no variable is left undeclared

It was not so obvious, however, to understand how to import typings from external libraries. With typescript 2 it’s more easy, since most of the time you simply have to install @types/something from npm registery in order to make things work.

All things considered, though, it was very fast and it took less than a week before we noticed how fast and secure it feels to code with typescript.

Conclusion

Benefits we observed about working in a typescript environment:

  • await/async made me change my mind about javascript. We now have something that looks that a good programming language, with concurrency support.
  • Adding tests is easier than before.
  • It is now tremendously easy to refactor something. Want to change the signature of a function ? simply rewrite and see the compiler tell you about every line of code that should be adapted. When the compiler’s finally happy, so are you, because it probably works.
  • The code is easier to understand. Functions are signed in their definitions, and interface make it really easy to know what methods/attributes are available on any object. Bonus: if you mess it up anyway, then the compiler will warn you.

Many thanks to everyone who are responsible for this typescript awesomeness. You guys rule.

Feel free to share some feedback about your ts transition if you have some !

Related links:


Published by HackerNoon on 2016/12/05