September 1, 2021

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.

Jakub Jakub Stasiak

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

> function getOrbit(impulse) {
... // A magic multiplier
... return impulse * 12.34;
... }

> // Impulse given in newton seconds
> getOrbit(20.25)
249.885
> // Good

> // Impulse given in pound-force seconds
> getOrbit(4.55)
56.147
> // Oops

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:

  • kgCO2e/t*km
  • gCO2e/t*km
  • kgCO2e/kWh
  • kWh/t*km
  • kgCO2e/passenger*km
  • ...

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).

Possible solutions

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).

function getTotalImpulse(): number {
    // Returns newton seconds
    return 20.25;
}

function getOrbit(impulse: number): number {
    // Returns kilometers
    return impulse * 12.34;
}

console.log(getOrbit(getTotalImpulse()));

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:

type NewtonSeconds = number;
type Kilometers = number;

function getTotalImpulse(): NewtonSeconds {
    return 20.25;
}

function getOrbit(impulse: NewtonSeconds): Kilometers {
    return impulse * 12.34;
}

console.log(getOrbit(getTotalImpulse()));

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):

type NewtonSeconds = number;
type Kilometers = number;
type PoundForceSeconds = number;

function getTotalImpulse(): PoundForceSeconds {
    return 4.55;
}

function getOrbit(impulse: NewtonSeconds): Kilometers {
    return impulse * 12.34;
}

console.log(getOrbit(getTotalImpulse()));

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.

type NewtonSeconds = number & { readonly __tag: unique symbol };
type Kilometers = number & { readonly __tag: unique symbol };

function getTotalImpulse(): NewtonSeconds {
    return 20.25 as NewtonSeconds;
}

function getOrbit(impulse: NewtonSeconds): Kilometers {
    return impulse * 12.34 as Kilometers;
}

console.log(getOrbit(getTotalImpulse()));

Looks neat. Let's try to break it again:

type NewtonSeconds = number & { readonly __tag: unique symbol };
type PoundForceSeconds = number & { readonly __tag: unique symbol };
type Kilometers = number & { readonly __tag: unique symbol };

function getTotalImpulse(): PoundForceSeconds {
    return 4.55 as PoundForceSeconds;
}


function getOrbit(impulse: NewtonSeconds): Kilometers {
    return impulse * 12.34 as Kilometers;
}

console.log(getOrbit(getTotalImpulse()));

This actually gives us a compile error:

yarn run v1.22.11

$ ts-node src/example-intersection.ts

/ts-node/src/index.ts:513
    return new TSError(diagnosticText, diagnosticCodes)
           ^
TSError: ⨯ Unable to compile TypeScript:
src/example-intersection.ts:16:22 - error TS2345: Argument of type 'PoundForceSeconds' is not assignable to parameter of type 'NewtonSeconds'.
  Type 'PoundForceSeconds' is not assignable to type '{ readonly __tag: unique symbol; }'.
    Types of property '__tag' are incompatible.
      Type 'typeof __tag' is not assignable to type 'typeof __tag'. Two different types with this name exist, but they are unrelated.

16 console.log(getOrbit(getTotalImpulse()));
                        ~~~~~~~~~~~~~~~~~

(...)


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:

type NewtonSeconds = number & { readonly __tag: unique symbol };
type PoundForceSeconds = number & { readonly __tag: unique symbol };
type Kilometers = number & { readonly __tag: unique symbol };

function getTotalImpulse(): PoundForceSeconds {
    return 4.55 as PoundForceSeconds;
}


function getOrbit(): Kilometers {
    return getTotalImpulse() * 12.34 as Kilometers;
}

console.log(getOrbit());

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:

type NewtonSeconds = number & { readonly __tag: unique symbol };
type PoundForceSeconds = number & { readonly __tag: unique symbol };
type Kilometers = number & { readonly __tag: unique symbol };

function getTotalImpulse(): PoundForceSeconds {
    return 4.55 as PoundForceSeconds;
}


function getOrbit(): Kilometers {
    const impulse: NewtonSeconds = getTotalImpulse()
    return impulse * 12.34 as Kilometers;
}

console.log(getOrbit());

The result:

$ ts-node src/example-intersection.ts

/ts-node/src/index.ts:513
    return new TSError(diagnosticText, diagnosticCodes)
           ^
TSError: ⨯ Unable to compile TypeScript:
src/example-intersection.ts:13:11 - error TS2322: Type 'PoundForceSeconds' is not assignable to type 'NewtonSeconds'.
  Type 'PoundForceSeconds' is not assignable to type '{ readonly __tag: unique symbol; }'.
    Types of property '__tag' are incompatible.
      Type 'typeof __tag' is not assignable to type 'typeof __tag'. Two different types with this name exist, but they are unrelated.

13     const impulse: NewtonSeconds = getTotalImpulse()
             ~~~~~~~

(...)


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:

// The required utility functions
function from<
    T extends { readonly __tag: symbol, value: any }
>(value: T): T["value"] {
    return value as any as T["value"];
}

function to<
    T extends { readonly __tag: symbol, value: any } =
    { readonly __tag: unique symbol, value: never }
>(value: T["value"]): T {
    return value as any as T;
}

// Our code
type NewtonSeconds = { value: number; readonly __tag: unique symbol };
type PoundForceSeconds = { value: number; readonly __tag: unique symbol };
type Kilometers = { value: number; readonly __tag: unique symbol };

function getTotalImpulse(): NewtonSeconds {
    return to<NewtonSeconds>(20.25)
}


function getOrbit(): Kilometers {
    return to<Kilometers>(from<NewtonSeconds>(getTotalImpulse()) * 12.34);
}

console.log(from<Kilometers>(getOrbit()));

It works correctly and prints the expected value. Let's break it (in the way that Intersection Types handled previously):

// (...)

function getTotalImpulse(): PoundForceSeconds {
    return to<PoundForceSeconds>(4.55)
}


function getOrbit(): Kilometers {
    return to<Kilometers>(from<NewtonSeconds>(getTotalImpulse()) * 12.34);
}

// (...)

Compilation fails (so: a success, really):

src/example-intersection.ts:26:47 - error TS2345: Argument of type 'PoundForceSeconds' is not assignable to parameter of type 'NewtonSeconds'.
  Types of property '__tag' are incompatible.
    Type 'typeof __tag' is not assignable to type 'typeof __tag'. Two different types with this name exist, but they are unrelated.

26     return to<Kilometers>(from<NewtonSeconds>(getTotalImpulse()) * 12.34);
                                                 ~~~~~~~~~~~~~~~~~


Now the second variant that the solution using Intersection Types doesn't handle too well:

// (...)

function getTotalImpulse(): PoundForceSeconds {
    return to<PoundForceSeconds>(4.55)
}


function getOrbit(): Kilometers {
    return to<Kilometers>(from<NewtonSeconds>(getTotalImpulse()) * 12.34);
}

// (...)

A compilation failure (or a success, as far as we're concerned) again:

src/example-intersection.ts:26:47 - error TS2345: Argument of type 'PoundForceSeconds' is not assignable to parameter of type 'NewtonSeconds'.
  Types of property '__tag' are incompatible.
    Type 'typeof __tag' is not assignable to type 'typeof __tag'. Two different types with this name exist, but they are unrelated.

26     return to<Kilometers>(from<NewtonSeconds>(getTotalImpulse()) * 12.34);
                                                 ~~~~~~~~~~~~~~~~~


This satisfies our requirements.

Conclusion

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!