Using an Int Type in TypeScript

The project I’m working on has a Node.js back end written in TypeScript, with a PostgreSQL database for persistence. We had a production user who encountered an error when the system tried to insert a row into a table that had an integer column in it. The value it was trying to insert was 239.99999999999997, so it made sense that PostgreSQL was complaining about it being “invalid input syntax for an integer.”

The Problem: Floating Point Numbers

The issue was caused by the fact that all JavaScript numbers are floating point numbers. And as Josh Clanton points out in the A Drip of JavaScript newsletter:

Due to the binary nature of their encoding, some decimal numbers cannot be represented with perfect accuracy. This is analagous to how the fraction 1/3 cannot be accurately represented with a decimal number with a finite number of digits.

The code that caused the problem was calculating a percentage (dividing two integers) and then multiplying that percentage with another integer. Due to constraints of the problem, the results were guaranteed (conceptually) to always be a whole number.

In this case, the calculation was 440 * (6 / 11). If you type this into a calculator, you’ll get 240. But if you open up a Node.js REPL and type it in, you’ll get 239.99999999999997. So even though I knew that the end result would always be a whole number, by forgetting about the inaccuracies of floating point numbers, I let a bug slip through. In order to ensure that the result is always an integer, as required by the PostgreSQL database, it needs to be rounded:


> Math.round(440 * (6 / 11))
240

The Solution: An Int Type

Forgetting to round the result of a calculation, when you know full well the result should be a whole number, is very easy to do. A teammate suggested that we use TypeScript to force us to round the result by coming up with an Int type we could use whenever a value was going to be inserted into an integer column in the database.

We decided to “brand” the number type in much the same way we had done for the DateStr type. Newer versions of TypeScript don’t like the enum style of branding, so we went with the recommended way and intersected with an object type:


export type Int = number & { __int__: void };

export const roundToInt = (num: number): Int => Math.round(num) as Int;

By properly typing the inputs and expected output of the calculation mentioned above, we would now get a compiler error:


const a: Int = 440 as Int;
const b: Int = 6 as Int;
const c: Int = 11 as Int;

const result: Int = a * (b / c);

The error was: Type 'number' is not assignable to type 'Int'. (In the real code, the numbers wouldn’t be hardcoded, and thus wouldn’t require the as Int. Rather, they might come in from a strongly typed GraphQL request, for example).

To keep the compiler happy, we needed to use the roundToInt helper:


const result: Int = roundToInt(a * (b / c));

We found it useful to have a couple of other helpers, as well. Here is the full module:


export type Int = number & { __int__: void };

export const roundToInt = (num: number): Int => Math.round(num) as Int;

export const toInt = (value: string): Int => {
  return Number.parseInt(value) as Int;
};

export const checkIsInt = (num: number): num is Int =>  num % 1 === 0;

export const assertAsInt = (num: number): Int => {
  try {
    if (checkIsInt(num)) {
      return num;
    }
  } catch (err) {
    throw new Error(`Invalid Int value (error): ${num}`);
  }

  throw new Error(`Invalid Int value: ${num}`);
};

BigInt

A BigInt type has recently been added to JavaScript (in May 2018 support for BigInt was part of the Chrome 67 release), so it is no longer true to state that all numbers in JavaScript have to be floating point numbers. Support for BigInt in TypeScript is on the roadmap for TypeScript 3.2. I suspect that once it’s available I can just change the definition of Int to this:


type Int = BigInt;

Conclusion

This Int type isn’t perfect, but using it will help the compiler prevent you from making the same kind of mistake we made when dealing with JavaScript numbers that really need to be integers.