We are the Dev Teams of
  • brands
  • ebay_main
  • ebay
  • mobile
<
>
BLOG

Type Safe JavaScript

by Frederik Leidloff

shape sorting cube

 

Don't you also feel that the lack of type safety is one of JavaScript's biggest drawbacks that prevents it from being a programming language for complex applications? Especially when writing Java code, I realize how I sometimes miss type safety in JavaScript. How many bugs occured because there was a type mismatch or some undefined behaviour?

When I argue with people about JavaScript, discussions often come to type safety. Indeed, JavaScript is a loosely typed language. Luckily, there are ways to make your applications type safe! In this article, I'm going to show you different approaches to type safety in JavaScript. All those approaches have the goal to make our code less likely to fail, more readable and thus easier to understand.

But what actually is Type Safety?

Before making our applications type safe, let's establish a common understanding of type safety. Defining type safety is not an easy task and as I learned while writing this article: There is not a single definition of type safety, it means something different in every language. What all definitions agree on is that type safety guarantees well defined programs. Type safety tries to minimize unexpected behaviour and gives us the possibility to argue about our code. Furthermore, IDEs (integrated development environment) have better support for type safe languages, because, for them it is also easier to deduce the meaning of code.

Let's consider type safety to be a feature that prevents us from doing type errors. It can be a feature of the programming language (e.g. Java) but also a feature of an application. Type safety can be static, catching type errors at compile time or dynamic, catching type errors at runtime. Type errors occur when there is a mismatch between an expected and the actual type of a variable. When those errors get caught, we can call an application type safe.

Type safety has nothing to do with whether a language is statically typed or dynamically typed. The difference between those two is: In a statically typed language, all types of variables are known during compile time and in a dynamically typed language, the types of variables are only known at runtime. However, it is important to understand that dynamically typed languages can be type safe, too. Examples are Python or Smalltalk.

Side note: Java is a type safe language but we can still run into unexpected behaviour, e.g. when a variable is null. Those NullPointer errors might not get caught during compile time and lead to runtime errors in the application.

Why do we need Type Safety anyway?

Consider the following example in JavaScript:

function add(a, b) {
    return a + b;
} 

The function is really simple and even though it consists of only one line, it can lead to unexpected behaviour:

add(1, 2) === 3;
add(1, "2") === "12";

Let's take a look at add(1, "2") === "12" to understand what's going on here. The + operand in JavaScript is defined for strings and numbers. When calling the add function with a number and a string, the number is converted to its string representation and the two strings are concatenated. The lack of type safety is the reason why we even have to think about those unexpected behaviours. In this article, I'm showing different ways how to achieve type safety in JavaScript by example.

Guava Style Preconditions

Even in a dynamically typed language, we have to worry about types. If the language itself doesn't care, we still care while writing functions, objects and modules. There is native support for type checking in JavaScript that apparently is a little bit confusing (see paragraph Native Support). The first approach to type safety in JavaScript that I present to you is using a library for type checking. One possibility is preconditions. It gives us Guava like checks that are easy to read and understand. Our add() function example with preconditions will look like this:

var preconditions = require("preconditions").singleton();
function add(a, b) {
    preconditions.shouldBeNumber(a);
    preconditions.shouldBeNumber(b);   

    return a + b; 
}  

Preconditions will throw an error when one of the conditions is not met. With such a library, we gain the power to argue about our code and something more: We can even add more complex checks to our code very easily. Imagine, we only want to add positive numbers, we can simply add preconditions.checkArguments(a >= 0 && b >= 0) after checking the type. Preconditions contains a rich set of convenience functions like shouldBeNumber, shouldBeDefined, shouldNotBeEmpty or shouldBeInfinite and many more. These functions make our code look nice and understandable. Take a look at them on their github page.

Alternative libraries

There many are other libraries for type checking, one example you might be familiar with is lodash. By the way, preconditions uses lodash under the hood.

Here at mobile.de, we recently used lodash in a project for type checking after a big refactoring. With _.isBoolean, _.isFunction_.isNumber and others, we gained confidence that we didn't mix up function parameters. However these convenience functions don't throw errors or stop code execution. What we did was using console.assert to print those errors to the console during testing:

var _ = require('lodash');

function add(a, b) {
    if (DEBUG) {
        console.assert(_.isNumber(a));
        console.assert(_.isNumber(b));
    }

    return a + b;
}

You can see in the code example that the implementation got clearer. By using a library that was already included, we didn't have to include additional dependencies to our platform and could still make function parameters explicit.

Making types explicit using type checks gives us, other developers and our future selves an easy way to understand input parameters of our functions. What we still don't have is proper support through our IDEs.

JSDoc

With type checks in functions, we gained the ability to argue about our code and write type safe functions. . What we still lack is IDE support that helps us write safe code and prevents us from doing silly mistakes that we would only notice earliest during testing. To get IDE support, we have to tell our IDE about expected types. One way to do that are structured comments using JSDoc. Let's take a look at the add() function with JSDoc:

/**
 * @param {number} a
 * @param {number} b - some comment about b.
 * @returns {number}
 */
function add(a, b) {
    return a + b;
}

We describe our input parameters as well as our expected output. The function itself looks clearer than the examples above because we don't change the function but only add some description above the function.

This way, our IDE can inform us about types of function parameters as we code and warn whenever we use incompatible types. Now we have IDE hints but except for some warning, nothing will happen when we write add(1, "2"). To really make use of those annotations and even get compile time errors we first need a compiler. The Google Closure Compilercan make use of JSDoc style comments and check for their validity during compile time. One drawback of using a compiler for JavaScript is that our build chain gets bigger. However, in times of really fast JavaScript task runners (Grunt,Gulp, ...) this should not be an issue. Especially not, when we gain type safety at compile time and hints from our IDE while typing.

Languages that compile to JavaScript

Besides the Google Closure Compiler and other static type checkers like Flow, there are also languages that compile to JavaScript. I'm talking about DartTypeScript and many more. These languages are super sets of JavaScript and compile to JavaScript. They add language features that are missing to the language. They all use a compiler and get compiled to valid JavaScript. TypeScript has type safety as a language feature. Let's take a look at our add() function example in TypeScript:

function add(a: number, b: number) {
    return a + b;
}

TypeScript allows us to define types as part of the language. TypeScript and similar languages have good IDE support and it is pretty easy to get started. There even is an online playground on the official TypeScript site to try out the language. One drawback of languages that compile to JavaScript is that they are more specialized than JavaScript itself and might miss essential features, especially new features from ES2015.

Native Support

The native support for type checking in JavaScript is very limited and sometimes confusing. In this part, I want to show you, why you should rather avoid using typeof or instanceof to create your own type safety checks. To do that, let's add typeof checks to our add() function:

function add(a, b) {
    if (typeof a !== "number" || typeof b !== "number") {
        throw new TypeError("a and b must be numbers");
    }
    return a + b;
}

Now, add(1, "2") will throw an error instead of leading to unexpected behaviour. And besides that we gain one more thing: We can argue about that function and any developer can know that a and b must be numbers. Apparently, that's not true. add(1, NaN) will not throw an error because typeof NaN === "number" (NaN stands for "Not a Number").

Furthermore, typeof is very limited to few types and in many cases typeof will just be "object" (e.g. typeof new Date() === "object").

There is a more explicit way of checking types in JavaScript with instanceof. With instanceof, our example would look like this:

function add(a, b) {
    if (a !instanceof Number || b !instanceof Number) {
        throw new TypeError("a and b must be numbers");
    }
    return a + b;
}

Instanceof is capable of understanding different object types, e.g. new Date() instance of Date === true. Instanceof checks for a constructor that can build the object in the object's prototype chain. That sounds more complicated than it is and apparently leads again to unexpected behaviour. Consider the following two:

"foo bar" instanceof String === false;
new String("foo bar") instanceof String === true;

The first string is not constructed using the String() constructor and that's why "foo bar" instanceof String === false.

When using typeof or instanceof we run into unexpected behaviour which makes our code more complicated. Even if we spend time understanding all the gotchas of typeof and instanceof, our future selves will most likely not understand the code to full extend. In order to keep our code clean, we should just not use those confusing parts of the JavaScript language.

Learnings

If you consider the lack of type safety to be one of JavaScript's biggest pain points, then it's up to you to do something about it. There are plenty of options on how to overcome the drawbacks of a loosely typed language, starting with code comments, adding type checks or even using a different language that compiles to JavaScript.

For existing legacy projects, I recommend to start using library driven type checks, for example with preconditions. You will gain type safe functions without touching the build chain. For new projects, you should always think about your possibilities and choose the appropriate one. No matter, how you tackle type safety, your goal should always be to write easily understandable and maintainable code.

How about you? I am looking forward to hearing about your experiences in the comments.

Acknowledgements

?>