Managing Dynamic and Weak Typing

JavaScript is a very flexible language, both dynamically and weakly typed. Let’s look at an example:

function add(a, b) {

return a + b

}

This function looks simple enough. We know it adds two things together and we know it returns the value of that statement. From the surface it seems relatively harmless, however without any more context we really don’t have enough information to make an informed decision about this method.

Because JavaScript is dynamically typed, we have no way of knowing the expected types without the author leaving a comment, and since it’s weakly typed we don’t know what side effects could manifest from unexpected type conversions. While this is a trivial example and we can probably manage most common edge cases, JavaScript has the ability to get quickly convoluted and hard to understand.

This is a common problem in JavaScript and one that has plagued applications since the beginning. This wouldn’t be an issue in a statically and strongly typed language like Java, because we would specify the types that the function would take as arguments, giving us the security of knowing we will never get unexpected types. It would look more like this:

public int add(int a, int b) {

return a + b;

}

The types that relate to this function are clearly defined. Making it apparent to any developer that it is in fact intended to add two numbers together. Not only that, but Java is strongly typed, so we can rest assured knowing types will never be coerced into other types.

For more information about strong vs weak and static vs dynamic typing I strongly recommend reading Premshree Pillai’s article on the subject.

So how can we make sure we don’t let dynamic typing bite us at runtime?

By following some best practices and making JavaScript’s dynamic nature work for us instead of against us. We do this by placing some conventions around how functions are declared.

Polymorphic Functions

One way to prevent issues with types is to make sure generic methods are polymorphic. Polymorphism allows our methods to be greedy, and handle different types properly. Let’s look at a polymorphic example of an add function:

function add(a, b) {

let result = undefined try {

if (typeof a === 'string' || typeof a === 'number')

result = a + b

else if (Array.isArray(a))

result = [].concat(a, b)

else

result = Object.assign({}, a, b)

} catch (e) {

console.log('Invalid types!', e.stack)

} return result

}

As you can see, this function is much more flexible. It allows for most common types and could be updated to include things like buffers or streams if the need arose and now we have a single add method that can be used for most purposes.

Polymorphism is a great way to reduce code repetition and make applications more flexible. Here is an example of a polymorphic map function:

function map(collection, iteratee) { // handle arrays

if (Array.isArray(collection)) {

return collection.map((item, index) =>

iteratee(item, index, collection))



// handle objects

} else {

const results = []

const index = 0



for (let key of Object.keys(collection)) {

results[index] = iteratee(collection[key], index, collection)

index++

}



return results

}

}

Generic functions like these really come in handy when you are using JavaScript in a functional manner.

TIP: If you want to learn more about polymorphism in JavaScript, have a look at this blog post by Toby Ho.

Assertions

Another way to manage types in JavaScript is to use assertions. Assertions allow us to throw an error if we receive unexpected arguments. This is a useful practice when you want to ensure that functions are not receiving dirty types.

Let’s update the add method to behave more like the Java example, we will ensure it only receives numbers using assertions:

import assert from 'assert'



function add(a, b) {

const invalidTypeMessage = 'Invalid type! Given: '



assert(typeof a === 'number',

`${invalidTypeMessage}${a.toString()}`) assert(typeof b === 'number',

`${invalidTypeMessage}${b.toString()}`)



return a + b

} try {

add(5, '4')

} catch (e) {

console.log(e.message, e.stack)

} return a + b

}

TIP: Be careful when implementing assertions, abusing them can lead to false positives. A good rule of thumb is to only use assertions when you are sure there is nothing else you could get than what you expect.

This will cause our add function to throw an error if given the wrong the type of arguments. We can then handle that error any way we want, either by letting it stop the application if the application can not run with the improper types here, or we can log the error and move on if it’s something non-critical.

Type Checking and Transpiling

The last way to ensure adherence to expected types is to use type checking. There are two ways to achieve this, one is to use a tool like Flow which lets you decide how intrusive/extensive you want your type checking to be, the other is to use TypeScript, which gives you static typing for JavaScript, like you expect from other statically typed languages.

First let’s take a look at adding Flow to the add method:

// @flow

function add(a: number, b: number): number {

return a + b

}

That’s all it takes, now if we had some offending code using this method incorrectly and we ran flow from the terminal, we would get an error informing us of the misuse.

Flow offers some other really powerful features like structural types for objects, so you can validate an object interface. This means that if some function requires an object with specific properties then any object that adheres to that naming can be used, but objects lacking the required properties will raise an error. This is at the heart of duck-typing, a very common pattern in JavaScript. It also allows the best of both worlds by allowing type checking where it matters but letting other parts of the application remain dynamic.

Now let’s take a look at how the add method would be implemented in TypeScript:

function add(a: number, b: number): number {

return a + b

}

Hmmm, that looks oddly familiar… As you can see Flow and TypeScript share the same syntax at the method level, but TypeScript offers far more than just type validation, it has support for more complex constructs as well. The major difference being there is no easing into TypeScript like you can with Flow. To clarify, I’m not saying Flow is better than TypeScript, only that they are different in their approaches.