Type safety, units and how (not) to crash the Mars Climate Orbiter
In this post I'll show how to handle units in a TypeScript codebase in a type-safe manner.
About that space probe
On September 23, 1999, NASA lost the Mars Climate Orbiter – a quite expensive piece of equipment, the cost of the whole mission was estimated to be $327.6 million – due to a programming error, namely a unit mismatch. A piece of ground software, supplied by a third party, calculated impulse generated by probe's thrusters in pound-force seconds, while NASA's trajectory calculation software expected the numbers to be in newton seconds – values half an order of magnitude different. As a result the spacecraft was lead down a trajectory only 57 kilometers away from Mars' surface – 23 kilometers lower than the estimated minimum safe altitude of 80 kilometers, almost four times lower than the originally planned optimal altitude od 226 kilometers – and either crashed or escaped is now orbiting the Sun.
This boils down to something as simple as
A costly and preventable mistake (which, to be fair, many are; preventable, that is, not necessarily that expensive).
Units at Lune
At Lune there are a lot of units used for various things:
- Kilometers and miles are used for distances (TODO: nautical miles)
- Kilograms and Twenty-foot Equivalent Units (TEUs, or: standardized 20-foot containers) for describing quantities of goods being shipped for emission estimates
- Kilowatt-hours, megawatt-hours and gigawatt-hours for describing the amount of electric energy used
- Grams/kilograms/tonnes of CO2 for purely carbon dioxide emissions and CO2e (CO2 Equivalent – this unit accounts for both carbon and non-carbon emissions, like methane)
There are also various derivative units coming from external datasets:
The units are normalized where possible (for example miles are converted to kilometers at the earliest possible stage and only kilometers are used internally) but that doesn't eliminate the problem, just makes it smaller and more manageable (the external databases operating using various derivative units are one case).
Let's use the simplified Mars Orbiter example as a starting point (we'll use TypeScript throughout this part, as this is what Lune mostly uses, although I wouldn't be myself if I didn't mention I introduced some Rust to Lune too and I hope to expand that in the future).
It prints 249.885 which is the value we expect. How can we prevent unit issues, like getTotalImpulse() returning pound-force seconds and getOrbit() treating that value as newton seconds?
It can be tempting to use TypeScript's Type Aliases:
The code runs and everything seems ok, but this runs too (and in the real world the functions will be separated by a lot of other code and the issue will be way less visible):
There are no compiler errors and the program prints 56.147`. The Orbiter crashes, game over. This is because, as the TypeScript documentation points out, Type Aliases don't actually create new types, just new names for them, which function merely as documentation and, just like documentation, can go out of date and become incorrect. This is no good.
What else can we do? Haskell has something called NewType, which is a construct that can create derivative types in a lightweight fashion and those derivative types maintain close and convenient relationship with their base type. While looking for typescript newtype on the Internet I found an interesting blog post by Dmitriy Kubyshkin which describes two ways of improving unit-related type safety in TypeScript. First of them uses Intersection Types and unique symbol in a way that makes the type different and compile-time, but at runtime it's just number. Let's modify our correctly working example from the above in this fashion.
Looks neat. Let's try to break it again:
This actually gives us a compile error:
It's not the best error message under the Sun but it'll do.
There's an important caveat though. Let's modify the last example in a way that makes getOrbit() call getTotalImpulse() on its own:
This will unfortunately compile and run (and print the wrong value, naturally). That's because at the call site inside getOrbit() there's no match against the NewtonSeconds type being attempted, the value is used just as a number – and as such it'll work. There are two ways to handle this issue. Either we modify the call site to have an explicit expected type specified:
It seems to do the job, unfortunately it still has a quite crucial downside: it's easy to forget about having to store the value in an appropriately typed variable/constant, the possibility of using getTotalImpulse() directly in getTotalImpulse() * someValue kind of fashion, where no unit errors will be caught, is significant.
Let's try the second solution from Dmitriy's blog post, called Fake Boxed Type. First a good version of the code with units matching:
It works correctly and prints the expected value. Let's break it (in the way that Intersection Types handled previously):
Compilation fails (so: a success, really):
Now the second variant that the solution using Intersection Types doesn't handle too well:
A compilation failure (or a success, as far as we're concerned) again:
This satisfies our requirements.
I hope I explained well why unit-related type safety is important and it's worth it to go the extra mile here.
It's not just academic talk – Lune already uses the Intersection Types-based approach in the codebase and I'm considering switching to the Fake Boxed Types for extra safety and maintainability – as a building block of many potential integrations Lune has to be as stable and reliable as possible.
Have fun with types and units!
Lune is on the mission to make every product and service climate positive by default. We are starting by offering the API for carbon removal and offsetting.
We’re always looking for the next great addition to our engineering teams at Lune.
If you care about tackling the climate crisis, check out our jobs page!