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.
- 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}
- 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.