My experience using static type checkers in JavaScript

To type check, or not to type check, that is the question

Adding type checking to your JavaScript app has pros and cons.

Pros

Catches most runtime errors before you ship them to production.

Gives you more context around runtime errors with types.

Frees up your mental energy. So you don’t have to keep as much code in your head around what the data structure of each variable is.

Improves code editor tooling. You can see errors immediately inline when you use a variable or method incorrectly. You also get autocomplete for object fields, methods etc. (including for libraries you import from npm if they have types available).

Basically, type checking in JavaScript has all the pros of type checking in any language.

Cons

But it’s not all sunshine and rainbows. Type checking has some cons:

Slows you down at first. If you aren’t trying to account for every situation, compiler errors can get in the way.

Can lead to a false sense of security. It only catches most runtime errors, not all – and it won’t catch logic errors where your code is wrong, just like with writing tests.

Because JavaScript doesn’t have type checking built-in, it comes with some extra cons as well:

Tooling is messy, especially with Flow (Flow libdefs are a pain).

Community is split between Flow and TypeScript, either way, you are buying into more non-standard syntax on top of JavaScript.

So overall, type checking has many pros and cons. But even with the pain points, I still feel adding type checking to your JavaScript apps is worth it. Just like with writing tests, it can provide huge value for shipping and refactoring with confidence.

Testing vs linting vs type checking

There are loads of tools available to improve your code quality.

A linter can be very helpful for catching a good chunk of common errors in your code. So can writing tests. So can type checking. But they all have a different purpose in my eyes:

Linting catches simple syntax errors. It can also enforce code formatting but I’d recommend an automatic formatter like Prettier instead of putting it in your linter.

Unit tests check input => output of chunks of code.

end-to-end tests check functionality works from the user’s perspective (here’s a post on how we end-to-end test the Ropig app).

Type checking catches more complex syntax errors and data structures used throughout your app.

I think all of these pieces can work to help improve the quality of your code. As your app grows and increases in complexity you can add on more tools to help. I’d recommend starting with a simple linter. Then add unit tests for pieces of stand-alone logic. Then add end-to-end tests as features come together. Finally, add a type checker as your data structures solidify.

A few tips for type checking in JavaScript

Whether you use TypeScript or Flow, here are a few tips I’d recommend after I’ve built a few apps with each:

Generate client types from back-end types where possible so you don’t have to maintain them separately. For example, on Ropig, we have a GraphQL API so we generate Flow types automatically from our API data structures (here’s a post about that).

Run your type checker on pre-commit and/or in CI like you do your tests/linter. This ensures your code is always clean and errors don’t build up. You don’t want to let errors build up or the type checker loses its value (just like with tests and linting).

Use codemods for updating your code between versions; this makes updates a lot less painful. For example, flow-upgrade if you are using Flow.

Use inference where it makes sense so you don’t have to maintain extra type-specific code. In my experience, this is an area that Flow shines in – you don’t need much type code for it to infer most things (but if any inference is costing you type safety, Flow won’t type check until you add explicit types).

Don’t spend too much time on type checking. If you find yourself wasting a lot of time with fighting the compiler, you either have bad code that you need to fix, or too complex of type code set up. You don’t want to get to the point where you are spending most of your time on tooling instead of fixing bugs and building cool features for your users.

TypeScript or Flow?

As with all tools, TypeScript and Flow each come with tradeoffs.

From what I’ve experienced, TypeScript has a more solid ecosystem and tooling but it requires a bit more buy-in (since it is a superset of JavaScript). Flow is messier but is more lightweight. I’d say if you are starting a new project, go with TypeScript. If you already have Babel compilation in place, Flow might be a good fit.

But they are both good tools that can help you write better code, so it doesn’t matter too much which one you pick. Every tool has pros and cons, there is no silver bullet. Don’t stress and fight about it too much 😉

Conclusion

Type checking is a double-edged sword. I think it is worth it, but because it isn’t native to JavaScript it does come with some baggage. I hope to see a day where JavaScript has native / first-class support for types. If you haven’t tried it out, give it a shot! I was pleasantly surprised at how much it improved my code 🙂