In Types We Trust

In Types We Trust

Posted by: Faris Keenan

The days of Javascript's confinement to small website enhancements is over. Now, JavaScript is the primary language of many large, and critical applications. To support this, the ecosystem has exploded with tools that reduce human error, increase readability, and encourage modularity. 

Amongst these are language features such as block scoped variables, constants, and modules. These reduce the occurrence of certain classes of bugs. But, by intentional design, JavaScript will always be dynamically typed: the type of a variable is determined at runtime, depending on the data it holds at the current point of execution. Consequently, Javascript applications remain exposed to a class of bugs caused by lax type safety.

Statically typed languages impose constraints on what passes as a valid program beyond mere syntax checks. For example, if you declare a variable of type boolean, and then invoke it like a function, your program will not compile as it is invalid in the language. Contrast this with JavaScript where such programs are valid, and result in runtime type exceptions. This is especially insidious as these bugs often hide in program states not covered by tests. With statically typed languages, we are given guarantees around type safety which prevent these bugs.

Whilst the degree of type safety is usually discussed as a language feature, it is also useful to consider a different perspective of looking at comparative type safety between programs. Two programs may be written in some language lacking formal type safety, but due to the use of assertions, type conversion, null checks, and general defensive programming, one can be more type safe than the other. Type safety on the language level puts a lower bound on type safety on the program level, the two are linked.

Taking this into account, the lack of type safety in the language cannot imply resulting programs have poor type safety, but its presence does imply that a resulting program has good type safety - assuming methods to circumnavigate type safety are not abused. Therefore, an argument of adopting static types is only sound when it is based on the fallibility of programmers introducing type related bugs, as opposed to an attack on dynamically typed languages.

This lower bound on the level of type safety of a program is imposed by the presence, and strictness of a static type checker. There are many choices when looking to introduce static type checking to your development stack. We use Flow at PassFort as it allows us to gradually type our existing JavaScript codebase over time, and has clever type inference which gives us immediate benefits before adding any explicit types. There are compile to JavaScript languages like ReasonML which have the advantages of much stronger static type checkers, and static types as a first class feature of the language, at a cost of a higher learning curve.

Consider the following example as persuasion for adopting a static type checker like flow.

The faulty chicken cooking time calculator

Let's create a calculator that will tell us how long we should cook a whole chicken for. The formula in plain English is: 25 minutes per 500 grams, plus an extra 25 minutes on top. So for a 500 gram chicken, the expected output is 50 minutes.

Here is a vanilla JS solution:

const parsedQuery = { chickenWeightInGrams: "500" } // Returns cooking time in minutes const cookingTime = (chickenWeightInGrams) => ((chickenWeightInGrams + 500)/500)*25 const output = cookingTime(parsedQuery.chickenWeightInGrams) //A console.log("Cook the chicken for:" + output)

 

Imagine that parsedQuery’s value is the result of parsing the query string ?chickenWeightInGrams=500.

Can you find the bug in the solution? Hints: It isn’t a runtime exception. It’s in the value of output+ is a polymorphic function ( its behaviour changes depending on the types of data input).

The value of parsedQuery.chickenWeightInGrams is the string "500". When the function + is given a string, and a number, Javascript’s dynamic typing coerces the number to a string, and concatenates the two. So, the value of “500” + 500 is “500500”. On the other hand, the division operator only applies to two numbers, so with “500500”/500, “500500” is coerced into a number, leaving us with the final output of 25025 minutes.

The fault here is not with dynamic typing, or Javascript, it has behaved as its specification promises. The fault lies with the programmer, forgetting to account for discrepancy between types. Some claim dynamic typing is advantageous as it abstracts away the need to think about types, but not thinking in this case has resulted in cooking a chicken for 17 days.

If only we had put this code through flow, it would have output the following compile time error: Cannot perform arithmetic operation because string is not a number.

It achieves this with type inference. Flow knows that (chickenWeightInGrams + 500)must be a number, as it is used in the divide function which only works with two numbers. This is only true if chickenWeightInGrams is a number, so the input to the cookingTime function must be a number.

Being informed by the type error, we find that we need to change line A to convert the string to a number:
(Number(parsedQuery.chickenWeightInGrams)).

Proponents of Test Driven Development may interject at this point, arguing that a prewritten test would have thrown a red flag. Not to downplay the importance of tests, or benefits of TDD, but I got the red/green signal for free. Also, your test itself is subject to the same type discrepancies as your code. What is testing your tests?

To elaborate on my point, in the real world, the value of parsedQuery would not be hard coded; but, the output of a function. In our statically typed codebase, we know this function returns an object with values, and keys of type string. Therefore, our compiler complains when passing a value which was output from the parsing function, to another function which expects a number. When writing a test, you must mock your input data, and it is easy to make the wrong assumption that the input to the function cookingTime is always a number.

With tests you make assumptions, with static type checking you enforce unbreakable contracts. TDD’s cousin, type driven development, catches a different set of bugs. There of course remains a need to write tests to check that our formula was codified correctly. The two techniques work together.

Type Driven development to the rescue

So far, we have let the type inference of flow guide us. This gets us a long way, but you can reap further benefits by taking the more proactive approach of writing explicit types.

Imagine we have a new requirement to be able to display the time to cook a chicken in hours, and seconds, as well as minutes. A big advantage of static type checkers is that they catch potential bugs at compile time, but a less touted benefit is that it positively influences design.

In type driven development, you approach your problem by first writing your types. After some thought, I arrive at the following structure:

type Grams = number type TimeUnit = 'HOURS' | 'SECONDS' | 'MINUTES' type Time = { time: number, unit: TimeUnit, }

 

These are type aliases. They work like variables, so I can now throw them around my code. The design is woven into the code as opposed to in comments, or a UML diagram to never be looked at again. TimeUnit is a union of string literals, like a set of enums. I represent time as an object composed of a number, and a unit describing that numbers meaning.

I update the cooking time function with explicit typings:

const cookingTime = (chickenWeight: Grams): Time => ({ time: ((chickenWeight + 500)/500)*25, unit: ‘MINUTES', //A })

 

Notice grams is no longer mentioned in input variable name, as the type Grams delivers that message. On line A, I hard code a string literal, if you change this to “nanoseconds” the compiler will throw an error as it is not of type TimeUnit.

const toHours = (time: Time): Time => { switch(time.unit) { case 'HOURS': { return time } case 'MINUTES': { return { unit: 'HOURS', time: time.time*60 } } case 'SECONDS': { return { unit: 'HOURS', time: time.time*60*60}} } return time // B }

 

Having decided on the shape of my types upfront, writing a function which converts different time units to hours becomes obvious. This is a perfect opportunity to return to the idea of varying lower bounds of safety a type checker will put on your program. Notice that all possible states of the value time.unit are covered in the switch, so line B is dead code. This return is required, as without it flow infers that the function may return a type Time, or a type Undefined, which is a mismatch with the functions explicit return type Time. If I remove the Minutes case the static type checker returns no warning. With a more powerful static type checker like that in ReasonML ( an OCAML dialect with a similar syntax to Javascript), I could omit line B, and the compiler would warn me if I miss a possible case. In the OCAML world, line B is bad practice as it is a catch all, one should instead cover every possible case explicitly. When using Flow you will encounter examples like B, and that is because the checker prefers soundness over completeness.

// TODO: implement minutes and seconds const convertTime = (time: Time, timeUnit: TimeUnit): Time => timeUnit === 'HOURS' ? toHours(time) : time

 

As an interface to toHours, convertTime is designed to take a time, and convert it depending on a given timeUnit which the user will input via a query string. A "TODO" comment has been left as a task to the reader. Don’t you feel so much more comfortable knowing that if you forget that the unit should be a plural, misspell an enum, have a branch which returns undefined, that the type checker will go red?

To get the output we desire we do some simple composition:

const output = convert_time(cooking_time(Number(parsedQuery.chickenWeightInGrams)), parsedQuery.timeUnit)

 

But wait, flow complains.
It seems that parsedQuery.timeUnit is a string which is incompatible with TimeUnit. Interpreting the compilers feedback, it is now obvious that we forgot to sanitise the user input. Deciding to handle erroneous input by defaulting to hours:
const sanitiseInput = (timeUnitInput: string): TimeUnit => { if(timeUnitInput === 'HOURS' || timeUnitInput === 'MINUTES' || timeUnitInput === 'SECONDS') { return timeUnitInput } return 'HOURS' } const sanitisedTimeUnit = sanitiseInput(parsedQuery.timeUnit) const output = convert_time(cooking_time(Number(parsedQuery.chickenWeightInGrams)), sanitisedTimeUnit)

 

From the example we have found that static type checking has:

  • Set a lower bound on type safety.
  • Allowed for type driven development.
  • Weaved documentation into the code.
  • Forced input sanitation.

The benefits of using flow extend beyond chicken metaphors, working at PassFort I have experienced these benefits first hand with the proof of success being fewer production bugs.

We are currently on the hunt for like-minded engineers to work with us at PassFort using technologies like Flow. If you are keen on seeing more click here.

Technology

Related articles