Type-checking your JavaScript makes coding fun again - Cogent
Type-checking your JavaScript makes coding fun again

Type-checking your JavaScript makes coding fun again

Craig Ambrose

Craig Ambrose

Crafter of code, and idealistic change-maker.

Modern static type checkers for JavaScript have come a long way, and you may just find they make you happier as well as more productive.

A lot of JavaScript developers are now using Flow or TypeScript to add gradual type-safety to their programming. Is this just a pointless craze, or is there something about type checking that makes it worth the extra trouble?

Greener fields

What is it about a brand new “greenfield” app that feels so much more fun to code than software with a year of work put into it? As we add features to our software, we start to feel the inertia of all the choices that we made slowing us down.

If you can’t tell if this is happening in your current code-base, imagine making a change to some important underlying data structure, or even just a far-reaching rename. Our ability to change what we’ve built is central to being able to continue to refactor the design and ensure that we face as little inertia as possible.

Using a dynamic language such as JavaScript, we sacrifice a lot of support for the safe modification of code, and in return we get to express ourselves quickly and easily in the beginning. So a greenfield app feels great, but even a couple of months of development later, we start to feel the pain.

Types of type checking

All programming languages deal with varying levels of strictness. JavaScript is a weakly typed language, because it does have types (numbers, strings, objects, etc), but they can be converted to or compared with other types. The ability to which type checking helps us change code, however, is less about whether types are strong or weak, and more about when they are checked.

Statically typed languages perform type checking at compile time, and fail to compile if you incorrectly substitute one type for another. Dynamically typed languages like JavaScript (and Ruby, Python, Elixir and many others) only perform type checks at run time.

Opening your parachute when you hit the ground

If you only check types when people actually run your app, does that negate all the benefits of checking at all? Not entirely, no. I found that when first moving from a statically typed language (C++) to a dynamically typed one (Ruby) I caught many potential errors while running unit tests.

Unit testing is an important safety net for many reasons other than just type checking, so you’re probably doing it already. If so, and if you have good coverage, then you may be getting a lot of the benefits of static type checking already, even though it’s happening dynamically.

Unit testing tends to be at its weakest when it comes to re-arranging how your units of code (classes, components, files, functions, whatever) connect to each other. This is the sort of problem that occurs if you re-name something, or change a major data structure (such as your Redux state, your API, or your database schema). We write tests to cover the integration of units of code, but they are often harder to write and they certainly don’t cover every branch of the code the way that unit tests do.

JavaScript or Ruby developers might laugh at languages like Java or C# for all their complex setup and boilerplate, but have you seen how easy it is to rename a class in those languages? There’s no regex searching through your code, it’s literally just right-click-and-rename and everything still works.

Revisiting static typing, my recent experiments with Elm

A while ago I built a small hobby web app using Elm for the front-end and Elixir (using the Phoenix framework) for the back-end. Both Elm and Elixir are modern functional language, with a notable difference around types. Elm is a statically, strongly typed language, whereas Elixir is a dynamically, strongly typed language.

Like many web apps, this project involved some data structures that made their journey through the entire stack, for example:

  1. As some stored data on the server
  2. Becoming structured in-memory objects in Elixir
  3. Getting serialised to JSON and sent over the wire to the client
  4. Getting parsed into in-memory objects in Elm and added to the state
  5. Being used to render view components

If you’ve never used Elm or Elixir this might sound a bit foreign, but the structure of such an app is almost identical to a React/Redux front end and a back end in any MVC framework (Rails, Django, .NET MVC, etc).

Elm spots a lot of my mistakes thanks to its type system.
Elm spots a lot of my mistakes thanks to its type system.

As the app grew, I started to make changes to the structure of data to suit either the front or back end, and typically I made those changes through the entire stack to keep things simple and consistent.

After adding quite a few features and making several such refactorings, I noticed something quite surprising, particularly given that I’m more familiar with Elixir than Elm:

Each change to the Elm code-base seemed to be just as easy as the last, whereas when changing the Elixir back-end I was starting to notice feelings of trepidation and confusion.

This led me to feel a lot happier when working with the Elm, despite the fact that it’s contained more verbose “boiler-plate”. I felt happier because I made smoother and more consistent progress and so I was more often in a flow state working on the Elm code.

So, is that the end of this article then? Forget about type checking your JavaScript, just use Elm instead? Maybe not.

When EXACTLY do I want type checking?

If I’m saying I was flowing better in the statically typed language than the dynamically typed one, then I guess that means it’s better to get type errors at compile time, but when exactly is that? As I work on the front end of an Elm (or a React) app, there are different levels of immediacy at which I might notice a problem.

  1. As I type each key in the editor
  2. When I’ve typed a complete chunk of valid syntax
  3. When I’ve saved the file (unless this is automatic)
  4. When the browser window I have open next to my editor hot reloads and displays my code
  5. When I run my tests (unless this is automatic)
  6. When I manually execute the feature in my web app
  7. When I commit to git (generally triggering my CI server to run the tests)

For dynamic typing, that means I may not find a problem until about step 6, unless my tests catch it in step 5. For static typing though, it happens at compile time, but which step is that exactly? Maybe it’s somewhere around 3 or 4, but notice I didn’t put in a specific compile step. Even in a compiled language, I certainly expect that to happen automatically for web development.

In fact, who says JavaScript isn’t a compiled language too. All my JavaScript apps are going through multiple compile processes these days, to process ES2015 code, to process JSX, and several other things — automatically whenever I save.

If the exact moment that compiling happens isn’t triggered explicitly, and maybe isn’t very clear, it just shows that it isn’t really what’s important here. What matters is my editing process, not my compile pipeline.

To flow best, I want to know about errors in my editor as I’m entering code.

This means, somewhere around step 1 or 2, with the ideal situation probably be not annoying me too much when I’ve only typed half a word, but also not waiting for some arbitrary save event.

In this React.js example, the addition of a type declaration indicating an expected property for one component immediately shows a flow error in the code that calls that component without the correct property (using Visual Studio Code).
In this React.js example, the addition of a type declaration indicating an expected property for one component immediately shows a flow error in the code that calls that component without the correct property (using Visual Studio Code).

Let’s talk about JavaScript

For static type checking in JavaScript we can use Flow or TypeScript. When you first add one of these to your project, there’s a command which is used to check the entire project for errors. When this is added to your test pipeline, this certainly gives you a an explicit step when your code is statically analysed that is much like the compiler failure you would experience in a language like Elm.

Next up though, you need to add relevant support to whatever editor you’re using. I don’t care about the specifics of your editor, but basically if you don’t get something drawing your attention to mistakes (in the same way that we’re now used to red squiggly underlines from spellcheckers), then you aren’t doing it right.

So here’s the big obvious take away. You don’t need Elm. You need edit-time type checking.

Type checking as you type will make you happier and more productive.

Type inference and gradual typing

Here I am raving about type checking, and yet I have no desire to go back to C++, or even C#. I like the simplicity of dynamic languages. It might seem like type systems that get bolted onto the outside of these languages are a bit of hack to rein in the cowboys — and to some extent that’s true! Whether by accident or by design, however, these type systems might actually be better than traditional statically typed languages.

Much of the time, when you code, the type system can figure out which types you mean. For example, Flow has no trouble with spotting this mistake even though no type definitions were provided:

const name = "craig"
const shoeSize = 11

if (name == shoeSize)

Flow knows the type of these variables because it sees the assignments. It can spot much more complex cases than this, because it allows that knowledge of types to “flow” through into all places where that variable is used, including when it gets passed through complex method chains.

So, modern type checkers can infer types, and this makes them so much easier to use, cutting down a lot of the programmer overhead needed to get the benefit of type checking.

If you aren’t already using one of them. Give Flow or TypeScript a go. It doesn’t matter which one. These aren’t enterprise level tools designed to play chaperone on untrustworthy programmers, they’re there to make you happier.

Related posts