Skip to content

Functional Programming in TypeScript Using the fp-ts Library: Option

Welcome back to our blog series on functional programming with fp-ts! In our previous post, we introduced the concept of functional programming and how it can be used to write more robust and maintainable code. Also we talked about the building block of fp-ts library: Pipe and Flow operators. Today, we're going to dive deeper into one of the most useful tools in the fp-ts toolbox: the Option type.

What is an Option?

In JavaScript, we often encounter situations where a value may or may not exist. For example, when we try to access a property of an object that may be null or undefined. This can lead to runtime errors and unexpected behavior. The Option type in fp-ts provides a way to handle these situations in a safe and predictable manner.

An Option is a container that can hold either a value or nothing. It is represented by the Option type in fp-ts, which has two constructors: Some and None. The Some constructor is used to wrap a value, while the None constructor represents the absence of a value.

  1. some: The some constructor is used to create an instance of Option when a value is present, or when a computation succeeds. It takes the value as its argument, and wraps it inside of the Option type.
import { some } from 'fp-ts/lib/Option';

const value = 42;

const optionValue = some(value);

console.log(optionValue) // {_tag: ‘some’, value: 42}
  1. none: The none constructor is used to create an instance of Option when a value is absent, or when a computation fails. It does not take any arguments; it simply represents the absence of a value.
import { none } from 'fp-ts/lib/Option';

const optionNone = none;

console.log(optionNone) //{_tag: none}  

The some and none constructors help us avoid null and undefined errors by forcing us to handle both cases explicitly using functional programming techniques.

But let's look at some examples:

import { Option, some, none } from 'fp-ts/lib/Option';

interface User {
	name: string;
	email: Option<string>;
	phone: Option<string>;
}

function getUser(id: number): User {
	// some logic to fetch user from database
	if (userExists) {
		return {
			name: userData.name,
			email: some(userData.email),
			phone: some(userData.phone),
			}
	} else {
		return {
			name: '',
			email: none,
			phone: none,
		};
	}
}

const user = getUser(123);

console.log(user.name);

if (user.email._tag === 'Some') {
	console.log(user.email.value);
} else {
	console.log('Email not found');
}

if (user.phone._tag === 'Some') {
	console.log(user.phone.value);
} else {
	console.log('Phone not found');
}

In this example, the getUser function returns a User object that may contain missing data. We use Option to represent the email and phone fields, which may or may not be present. We can then pattern match on the Option to safely handle the case where the field is missing.

Pattern matching is a powerful feature in functional programming that allows developers to match values against a set of patterns and execute corresponding code based on the match.

We are pattern matching Option when we do this in the example above:

if (user.email._tag === 'Some') {
	console.log(user.email.value);
} else {
	console.log('Email not found');
}

Why use Option?

Using Option can help us avoid runtime errors and make our code more robust. By explicitly handling the absence of a value, we can prevent null or undefined errors from occurring. This can also make our code easier to reason about, as we don't have to worry about unexpected behavior caused by missing values.

Option can also help us write more expressive code. By using Some and None instead of null or undefined, we can make our intentions clearer, and reduce the cognitive load on other developers who may be reading our code.

Let's say we have a function findUserById that retrieves a user from a database based on their ID. If the user is found, the function returns the user object. But if the user is not found, it returns null.

function findUserById(id: number): User | null {
	// Code to fetch user from the database
}

const userId = 123;

const user = findUserById(userId);

if (user !== null) {
	// User found, do something with the user object
	console.log(user.name);
} else {
	// User not found
	console.log("User not found");
}

In this example, we explicitly check for null to determine if the user was found or not. This approach can lead to potential runtime errors if we forget to check for null in some parts of our code.

Now, let's rewrite the same example using fp-ts Option:

import { Option, some, none } from 'fp-ts/lib/Option';

function findUserById(id: number): Option<User> {
	// Code to fetch user from the database
} 

const userId = 123;

const userOption = findUserById(userId);

userOption.fold(
	() => {
		// User not found
		console.log("User not found");
	},
	(user) => {
		// User found, do something with the user object
		console.log(user.name);
	}
);  

In this example, the findUserById function returns an Option<User>, which can either be some(user) if the user is found, or none if the user is not found.

Instead of manually checking for null, we use the fold method provided by fp-ts Option. If the user is found (some(user)), the second function inside fold is executed (right function), and if the user is not found (none), the second function is executed (left function). This approach ensures that we handle both cases explicitly, avoiding potential null-related errors.

The fold method is a fundamental operation provided by the Option type in fp-ts. It allows us to extract values from an Option instance by providing two functions: one for handling the case when the Option has a value (Some), and another for handling the case when the Option is empty (None).

It's important to note that Option in fp-ts is implemented as a discriminated union, meaning the some and none constructors are different variants of the same type. This enables the compiler to enforce exhaustiveness checking, ensuring that we handle both cases when using fold or other methods that require pattern matching.

Conclusion:

In this post, we've introduced the Option type in fp-ts and shown how it can be used to handle null or undefined values in a type-safe manner. We've also seen how Option can be used to represent optional values in a more self-documenting way. In future posts, we will explore the fold method in more detail with examples, along with other useful Option methods combined with other new fp-ts operators. These techniques will further enhance our functional programming skills and allow us to handle Option instances more effectively.