Using options to eliminate null checks from your codebase.
Created Tue, Aug 18, 2020
The existence of null has been the oldest and the most notorious source of runtime
errors. Null has plagued our code with a never ending chain of null checks. It is
time we eliminate null by using techniques that have been shown in languages like
Rust
and Haskell
.
Here are some programming languages that don't have null
In the JavaScript/TypeScript world things are particularly worse because we have two separate
types of null,null
and undefined
.
null == undefined; // truenull === undefined; // false
typeof null; // => "object"typeof undefined; // => "undefined"
This article goes deep into this topic. Click Here
In this article I will show you how to eliminate null from your code by using a simple implementation of the option type from rust in typescript.
I have been learning rust for a couple of months now and I wanted to introduce one of the great features of rust to TypeScript and JavaScript. So I created the optionem library.
The option type is used in places were some operation will return either a value or no value. Null is often used to represent the no value case independent of the type of the value to be returned. We have seen that this leads to unexpected problems if the programmer calling that operation forgets to handle the no value case.
For example let's look at a Map
.
interface Map<K, V> { // .. get(key: K): V | undefined; // ..}
The Map interface has a method called get. The functionality of get is returning a value stored inside the map using the key associated with the value otherwise if there is no value stored in the map with the given key it returns undefined.
Now lets see what happens when we call get in our code.
const map = new Map();const value = map.get("key");
console.log(value.length); // ERROR
This returns an error because we forgot to check for undefined. This is a dumb example because if you use typescript you wouldn't make this error. But it shows how forgetting null checks causes runtime errors.
const map = new Map();
map.set("key1", "");map.set("key2", "value");
if (map.get("key1")) { // doesn't execute console.log("value exists at key1");}
if (map.get("key2")) { // executes console.log("value exists at key2");}
if (map.get("key3")) { // doesn't execute console.log("SHOULD NOT RUN: no value at key3");}
This code produces the following output
value exists at key2
If we look at the third if-condition we can see that we didn't have a value with the key key3 so the
get method returned an undefined and due to undefined being a falsey
value in javascript
the if-condition didn't get executed.
Now lets look at the first if-condition. the value at the key key1 is an empty string and empty string
is a falsey
value, so even though the get method returned a value the value returned was also
falsey
thus the if condition didn't get executed.
if ("") { console.log("SHOULD NOT RUN");} else { console.log("empty string is falsey");}
To fix the bug in the first if-condition we need to explicitly check for an undefined.
const map = new Map();
map.set("key1", "");map.set("key2", "value");
if (map.get("key1") !== undefined) { console.log("value exists at key1");}
// ...
As we can see in the above example we had to jump through a lot of hoops to get the functionality we wanted. Now let see how much the Option type would be useful in our example. We will use the option type from the optionem library.
An implementation of Map that returns an option.
import { Option } from "optionem";interface OptionMap<K, V> { // ... get(key: K): Option<V>; // ...}
First Let's see what option is.
The option type represents an optional value. An option is either a Some
that contains a value or a
None
that does not. The option type is used in places where in normal circumstances you get returned
a null or undefined if the operation failed.
Think of them this way. The option type has two variants the none and some variants.
interface Option<T> { // ...}
class Some<T> implements Option<T> { // ...}
class None<T> implements Option<T> { // ...}
Example of using options
import { Option } from "optionem";
const some: Option<number> = new Some(10);const none: Option<number> = new None();
Now let's return to our map example.
class OptionObjMap<K, V> implements OptionMap<K, V> { // ...}
const map = new OptionObjMap<string, number>();
map.set("key1", 10);
map.get("key1").match({ Some(val) { console.log("key1", val); }, None() { console.log("key1: None"); },});
map.get("key2").match({ Some(val) { console.log("key2", val); }, None() { console.log("key2: None"); },});
The above code produces the following output
key1 10key2: None
The match method on options takes in an object with two functions namely None and Some. The functions are called in different ways depending on which variant of the option the match method was called on:
By using the the match method of the option we can handle both variants of the returned Option declaratively. We can see that using has made our code more readable.
There is also another type that I have observed to be very useful in rust. It is the Result type. I am planning on implementing the result type in optionem. Another missing component is being able to pass async functions to methods on the option interface.
Nonetheless it is a really cool simple library use it in your projects, start it on github and Thank you for reading this article.
All content in this website by Fraol Lemecha is marked with CC0 1.0 Universal.
By marking the work with a CC0 public domain dedication, the creator is giving up their copyright and allowing reusers to distribute, remix, adapt, and build upon the material in any medium or format, even for commercial purposes.