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.
Types of type checking
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.
Revisiting static typing, my recent experiments with Elm
I recently 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:
- As some stored data on the server
- Becoming structured in-memory objects in Elixir
- Getting serialised to JSON and sent over the wire to the client
- Getting parsed into in-memory objects in Elm and added to the state
- 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).
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.
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.
- As I type each key in the editor
- When I’ve typed a complete chunk of valid syntax
- When I’ve saved the file (unless this is automatic)
- When the browser window I have open next to my editor hot reloads and displays my code
- When I run my tests (unless this is automatic)
- When I manually execute the feature in my web app
- 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.
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.
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) doSomething() }
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.