May 15, 2018

Choosing (and using) Javascript static typing

Choosing (and using) Javascript Static Typing | Mixmax

Overview

We love Javascript at Mixmax, and some specific attributes we love are its flexibility and speed of development. This helps us ship and iterate on new features quickly. However, one language feature that quickly shifts from improving to impeding developer speed is Javascript’s lack of a type system. What starts as not having to worry about types and function signatures quickly turns into confusion and bugs. That’s where a static type system comes to the rescue, improving developer confidence and mitigating type errors.

When does static typing help?

For a while, descriptive variable names, unit tests, and solid documentation can provide enough type information to passably avoid adopting a static type checking system, but eventually, more complex functions and a larger codebase strain. At Mixmax, we used common code styles (both linting and best practices) and documentation formats (we like JSDoc best) to make code clear and readable. As an example, a function definition might look something like this:

/** * Retrieve a contact related to the given email * * @param {String} email * @return {Promise<Object>} */
async findByEmail(email) {
  /* finding one... */
}

For a function with one argument and a simple return value, this works great. It’s clear what the function requires and returns. However, a few versions later, with more requirements, the same function has added a bit of complexity and now looks like this:

/** * @param {String} email * @param {Object} [query] * @param {Array<String>} [fields] * @param {Object} [options] * @prop {Number} [options.limit] * @prop {String} [options.paginatedField] * @prop {String} [options.next] * @prop {String} [options.previous] * * @return {Promise<Object>} * @property {Array<Object>} results * @property {String} next * @property {String} previous */
findByEmail(email, query, fields, options = {}) {
  /* find by email… */
}

The documentation has quickly gotten out of hand with three optional values, multiple nested attributes and unclear return values (what does the return object have if there are no results?). It would be easy to call this function and forget the order of arguments and required properties which changes the behavior of the rest of the code. This is where static type checking comes in handy.

Static type checkers at a high level go through your code and warn of invalid function calls, object descriptions, and other mistakes that wouldn’t be caught with just linting.

Choosing a type system

Of course, we’re not the first to run into these problems with an untyped language, so there are already a few well-supported options for adding a types to existing Javascript projects. Two options that are widely used and well supported are Flow and TypeScript. Though the end goals of Flow and TypeScript are similar (making sustainable, large Javascript projects), they each accomplish their goals very differently. To evaluate the differences between Flow and TypeScript, we had 3 questions on which to evaluate them:

  • How can we incrementally adopt them (and revert if necessary)?

  • How do they perform on hundreds of files in one project?

  • How will they change how we write Javascript?

Incremental Adoption

As a small (but growing) engineering team, it’s important that whichever system we choose provides immediate benefits to productivity and reliability rather than requiring a month-long rewrite or a steep learning curve. Luckily, both Flow and TypeScript support file-by-file transitions, so neither would’ve forced us to rewrite entire projects at once (whew).

Additionally, we wanted to be able to easily switch back from either TypeScript or Flow to vanilla Javascript. This way, if we started using one type system and found something different or better we wouldn’t be locked in by the choice. Here there’s a little difference between the two.

Switching away from Flow would be easy, since it’s just transpiled using a babel preset. The results of this transpilation maintain comments, spacing, and other nice-to-haves, just without Flow’s type annotations. As a result, switching from Flow would just be a matter of renaming directories and removing Flow-enabled files.

On the other hand, switching from TypeScript wouldn’t be such a breeze, as TypeScript is a different language that’s compiled (rather than transpiled) to Javascript. As a result, the .js files created by TypeScript’s compiler lacked much resemblance to the original code (removing spacing, comments, etc.). This meant a high barrier to switching back, and switching required some other tool or manually migrating.

Performance

Prior to using either Flow or TypeScript, it was important for us as a team to understand the impact our choice would have on our build times and in our editors. Primarily, we wanted to ensure that adding a type system to our build process wouldn’t bloat build times and that it wouldn’t slow down local development constantly waiting for slow compilation (or transpilation). While there aren’t a ton of benchmarks available, there’s some evidence that Flow increases compilation time for every file added and TypeScript does not. Though, in practice, there is no performance difference between the two.

Additionally, given that both frameworks are used successfully in large projects, we’re willing to call this one a draw. One thing we noted is that both TypeScript and Flow offer options to only check changed files which is great for keeping development snappy.

Project Philosophy

Up to this point, Flow slightly edged out TypeScript for our purposes, offering less risk of adoption it. But our final question, “how will this change how we write Javascript,” proved the most influential in our decision.

To contrast them directly, TypeScript on one hand is a fundamentally different way of writing Javascript, adding lots of syntactic sugar and typing features, to the point where it no longer has the same feel as the Javascript we usually write. The additional features can be nice, sure, but are orthogonal to the problem that led us to typing in the first place.

On the other hand, Flow fit nicely into our existing methods, conventions, and best practices. We didn’t have to learn completely new syntax for the benefits of a type system to be immediately apparent. It also let us be as type safe as we wanted. In testing, we found that Flow provided more guarantees without requiring extremely detailed definitions, while TypeScript generally lagged behind in that regard.

Finally, and most importantly, Flow is built to optimize type-checking, while TypeScript is a different flavor of Javascript that happens to have types. Exemplifying this is Flow’s type inference that immediately provides more feedback on unsafe portions of code. While TypeScript also provided immediate type checking, it checked and found fewer unsafe uses in general.

In the end, Flow’s drop-in benefits, focus on type-checking, and low transition cost convinced us to start using it throughout our code base. After 4 months, it’s serving us well and continuing to see widespread use throughout our npm packages!

If you want to write some type-safe Javascript, come join us at Mixmax! We’re hiring!

You deserve a spike in replies, meetings booked, and deals won.

Try Mixmax free