Get Better Type Checking in JavaScript with the Maybe Type

July 05, 2018 0 Comments

Get Better Type Checking in JavaScript with the Maybe Type

 

 

The dynamic typing in JavaScript makes it incredibly flexible. But, this flexibility can act as a double edged sword. When values have the potential to change, JavaScript will give you runtime errors in your code or some crazy bugs that will take forever to track down because of type coercion.

One way to tackle this issue is to fill your code with conditionals for null and undefined values and type checks. But this will make your app’s logic that much more complex to read and refactor at a later time.

Maybe type is a popular abstraction for defining values that may or may not exist. It encapsulates the type checking and guards our code against any error or bugs caused by the missing values.

In this post, I will show how to use Maybe and keep your app readable and maintainable. Let’s dive in.

Create an empty folder in your system. You may name it anything you want.

$ mkdir maybe
$ cd maybe

Open this folder in a code editor. I like to use VS Code.

$ code .

Create a file named utils.js file and write a simple utility function inside it.

const inc = n => n + 1
module.exports = {
inc
}

Here, the inc function takes in a number, increments it by one and returns it as the result.

Create another file called index.js and import the inc function inside it.

const {inc} = require('./utils')
const input = 2;
const result = inc(input);
console.log(result);

Run this code in your terminal using the command node index.js and you will get the output as 3.

But what if you give a string as the input? In the index.js file, pass any string to the input and run the node index.js command.

What you will get the as the output is the same string, but with a 1 attached to it. This happens because the plus operator now acts like a concatenator.

Change the value of input to undefined, and now your result will be a NaN. To avoid this obstacle, we can add a conditional statement that checks the typeof the input.

const result = typeof input = 'number' ? inc(input) : 0

While this works, we can do something much better with the help of maybedata type. Import a new library into this module called crocks.

$ yarn add crocks

This library contains the Maybe data type inside it. Import this library inside index.js file.

const Maybe = require('crocks/Maybe')

Maybe is an object that wraps a value and lets us differentiate between values that we want to act on and value that we do not want to act on. The reason this works is that Maybe is made up of two underlying types and it can be any one of those at a time.

Maybe is made up of a Just and Nothing type. Just is a value that we want our code to work on, and Nothing is the value that we want our code to ignore.

Inside index.js file, rewrite the code inside it using the Maybe data type.

const input = Maybe.Just (2);
const result = input.map (inc);

Running this code will get us the result as Just 3. What happened is that we wrapped up our 2 in this Maybe. Then, when we called map on it to apply to that function, the Maybe unwrapped the value, got the 2 out and passed it into the inc function. The inc function then incremented it by 1, and passed it to result and wrapped it back up in the Just.

Replacing the value inside input with Maybe.Nothing() will give us the result as Nothing.

To use both of these types simultaneously, create a new function inside index.js called num and pass it to the input.

const num = val => typeof val = 'number' ? Maybe.Just(val) : Maybe.Nothing();
const input = num(2);
const result = input.map(inc);
console.log(result);

Passing any type of data that is not a number will be skipped over by the function that expects only a number and yield us a Nothing. Using the maybetype, we've added an element of safety to a function without having to change that original function.

Create another utility function inside the utils.js file called upper.

const upper = s => s.toUpperCase();
module.exports = {
inc,
upper
}

Import this utility function in the index.js file and use it as shown below:

const { inc, upper } = require('./utils');
// previous section's code
const input2 = 'bit';
const result2 = upper(input2);
console.log(result2);

Run this code using node index.js command and you will notice that the value of result2 is BIT.

But what if I pass something other than a string to input2? The output will then be a s.toUpperCase is not function.

To solve this issue, we can create a new function that takes in a val and checks its type to see if it is a string. I will then pass it to input2.

const safe = val => typeof val = 'string' ? Maybe.Just(val) : Maybe.Nothing();
const input2 = safe('bit');
const result2 = input2.map(upper);

Now if you pass a value like 5 to input2, you will get the result as Nothing. The only problem we have now is that the safe function is not generic enough. Create two new functions that check if the type is a number or a string.

const number = val => typeof val = 'number';
const string = val => typeof val === 'string';

Create another function that takes in a predicate function and a value. All we are going to do here is call the predicate function on the value, and based on the result, this function will either return a Just or a Nothing.

const safe = (pred, val) => pred(val) ? Maybe.Just(val) : Maybe.Nothing();

Instead we can now call the new function safe in our code.

const input = safe(number, 2);
const result = input.map(inc);
const input2 = safe(string, 'bit');
const result2 = input2.map(upper);

Currently we are getting the output as either a Nothing or a Just followed by some data. But we don’t want our data to have these words associated with them. We want our data to be raw so that we can easily use it somewhere else.

Let’s create a new utility function inside the utils.js file that takes some number as input and doubles it.

const double = n => n * 2;
module.exports = {
inc,
upper,
double
}

I will use it in the index.js file to double the output of the inc function.

const {inc, upper, double} = require('./utils');
// previous section's code
const doubleOfInc = double(result);

If you run this code, you will get the output as NaN. That is because the result contain Just 3 and the double function only takes in a number as an input.

We can use Maybe to solve this problem. Restructure the doubleOfInc as a function like this:

const doubleOfInc = n => {
const safeNumber = safe(number, n);
return safeNumber.map(inc).map(double);
};
const result3 = doubleOfInc (2);
console.log(result3)

Even though we are getting a proper result now, it is still wrapped in a Justor Nothing. To get an unwrapped result, we need to use .option.

const doubleOfInc = n => {
const safeNumber = safe(number, n);
return safeNumber.map(inc).map(double).option(0);
};
const result3 = doubleOfInc (2);
console.log(result3)

The .option is a method that takes in a default value that will be returned to us in case of a Nothing. If our result has a Just in it, then it will unwrap the output and only give us that.

Inside the index.js file, create an object with a few properties inside it.

const time = { 
date: 5,
month: 7,
year: 2018
}
const date = time.date;
const month = time.month;
const year = time.year;

I can now use the upper function to turn the string to an uppercase. But that will not work if this object did not have the property that I am passing to the function. If that is the case, I will get an undefined instead.

To solve this, you can pull in the safe utility from the crocks property. This utility function is a lot similar to the one that we had used in the earlier sections.

const safe = require('crocks/Maybe/safe');

Since the safe function requires a predicate function, pull in a few more utility functions from the ramda library. Also install the ramda library using NPM/Yarn.

const { compose, isNil, not } = require('ramda');

Create a new function inside the index.js file. I will call it isNotNil and set it equal to compose, and compose will call the not and isNil functions.

const isNotNil = compose(not, isNil);

Now, rewrite the date constant using safe with isNotNil.

const date = safe(isNotNil, time.date);
const nextDate = inc(date);
console.log(nextDate);

The output will now be something like Just 51. This is not what I expected it to be…

We can also use prop from the crocks library. This is a generic utility function that will grab any property from any object. By using this, we can comment out the imports from ramda and the isNotNil predicate function.

const prop = require('crocks/Maybe/prop')

This function will pull the property off the object, and if it is not nil then it will give us a Just wrapping the value that was in that property. But if it is nil, we will get a Nothing.

const date = prop('date', time);

The added benefit here is that we can call prop with just one argument. This argument will the property name. This way we get a reusable utility function.

In JavaScript, there are a lot of places where we are required to handle nullable values. For example, null values need to be managed when we need to access a value from an array with bracket notation or Array.prototype.find.

By using Maybe in place of nullable values, we do not have to worry if the value exists or not. Maybe takes care of it for us.

There are many more advantages and disadvantages to using a Maybe type in JavaScript. Try out the code in the post, figure out how else you can use Maybe in your code, and get back to me with your thoughts/comments…


Tag cloud