Using Option with fp-ts in your TypeScript code

Michael
6 min readJan 29, 2023

--

(fp-ts series 1: Container)

  1. 👇🏻 Option
  2. Either
  3. TaskEither (part 1) — Basic introduction
  4. TaskEither (part 2) — Dependent Promises
  5. TaskEither (part 3) — Independent Promises
  6. TaskEither (part 4) — More examples

Option is Data Structure (Container), which helps you to store data with two possible states: the value is absent (None) or the value is present (Some). We can use Option to represent an optional value in an Object in JavaScript.

For example, let's say we have a Person Object Type and an Address Object Type:

type Address = {
addressLine1: string;
addressLine2?: string;
};

type Person = {
name: string;
age: number;
address?: Address;
};

The address property is an optional object and the addressLine2 property is an optional string in the address object. Suppose you want to access the value of addressLine2, a function called getAddressLine2 is created in normal TS code:

const getAddressLine2 = (person: Person) => {
// To get the address line 2,
// Need to check whether the value is empty (null or undefined or "")
if (person.address && person.address.addressLine2) {
return person.address.addressLine2;
}
// Return an empty string as the default value;
return "";
};

Since both the address property and the addressLine2 property are optional values, we can use Option to represent the data:

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
/*
Step 1: create an Option container and wrap the data inside Option becasue address can be empty
Use the Option constructor "option.fromNullable"
*/
const addressOption = option.fromNullable(person.address);
// Extract the value of addressLine2 from addressOption ...
};

Now, the addressOption has a type signature — Option<Address>. In other words, the value can be None or Some(Address).

To access the addressLine2 inside the address Object, intuitively we want to do something like addressOption.addressLine2.

However, we could not do that because the value of address is wrapped in Option, so we need to unwrap the address from Option and then access the value of addressLine2.

Also, addressLine2 should also be wrapped in Option because the value could be empty. Hence, if we are able to obtain the non-empty value of address, we could do the same for addressLine2 using option.fromNullable(address.addressLine2) .

The following code snippets display two approaches to obtain the non-empty value of address in order to get the value of addressLine2:

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
/*
Step 1: create an Option container and wrap the data inside Option becasue address can be empty
Use the Option constructor "option.fromNullable"
*/
const addressOption = option.fromNullable(person.address);

/*
Step 2: Extract the value of addressLine2 from addressOption
Approach 1 -
Use "option.isSome" to extract the value
*/
if (option.isSome(addressOption)) {
// The address value can be obtained by .value
const address = addressOption.value;

// Wrap the addressLine2 with Option
const addressLine2Option = option.fromNullable(address.addressLine2);
}
// Return the value of addressLine2 ...
};
import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
/*
Step 1: create an Option container and wrap the data inside Option becasue address can be empty
Use the Option constructor "option.fromNullable"
*/
const addressOption = option.fromNullable(person.address);
/*
Step 2: Extract the value of addressLine2 from addressOption
Approach 2 -
Use "option.map" to extract the value and wrap the addressLine2 with Option
*/
const addressLine2Option = option.map((address) => option.fromNullable(address.addressLine2))(addressOption);

// Return the value of addressLine2 ...
};

I would recommend using the second approach because it is more functional (You came here to learn coding in a more Functional Programming way, right? :)). Another article will be published to explain the .mapfunction in depth because this is related to a very important and core concept (Functor) in Function Programming.

To explain .map briefly, it helps you to unwrap the value inside the container and perform some transformation with the data. (Looks familiar with the .map function when you work with Array in JS, right ?:))

Oh wait… if you check the type of addressLine2Option, it is Option<Option<String>>, the addressLine2 value is wrapped an Option wrapped in another Option! But what we want is only Option<string>!

If you look at the type signature (type signature is VERY IMPORTANT in Functional Programming) of .map in Option, which is f:(a: A -> b: B) -> Option<a: A> -> Option<b: B> .

It works like this: First it unwraps the data a from Option<a: A>, feed the value a as an input to f, transform a into b, then wrap the result b with Option and return it. (Note: If Option<a: A>is None, the function f would not be triggered and the origin value None is returned. )

This is why earlier we get Option<Option<string>> instead of Option<string> !

Luckily, we can fix it easily! Since both layers are Option, we can use .flatten to remove the outer layer:

 const addressLine2Option = option.map((address) => option.fromNullable(address.addressLine2))(addressOption);
const addressLine2OptionForReal = option.flatten(addressLine2Option);

An even more functional way is to use .chain (I know, sorry :[ ):

const addressLine2Option = option.chain((address) => option.fromNullable(address.addressLine2))(addressOption);

In short, .chain(.bind is used for another purpose in fp-ts) is a flattenMap function that performs .map then .flatten. Again, .chain function is related to another big topic (Monad) in Function Programming, I will write another article to explain it in depth.

Finally, we should return the value of addressLine2 with the default value “” if the value is empty by using option.getOrElse:

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
/*
Step 1: create an Option container and wrap the data inside Option becasue address can be empty
Use the Option constructor "option.fromNullable"
*/
const addressOption = option.fromNullable(person.address);
/*
Step 2: Extract the value of addressLine2 from addressOption
Approach 2 -
Use "option.chain" to extract the value and wrap the addressLine2 with Option and then flatten the result
*/
const addressLine2Option = option.chain((address: Address) => option.fromNullable(address.addressLine2))(addressOption);
/*
Last Step: Return the value of addressLine2 with the default value "" if empty
*/
const result = option.getOrElse(() => "")(addressLine2Option);
return result;
};

const personA = {
name: "A",
age: 20,
address: {
addressLine1: "hehe",
addressLine2: "haha",
}
}

// You should get "haha"
getAddressLine2(personA);

I will leave you to test the getAddressLine2 function with different inputs (empty address and empty addressLine2).

FYI, I would use pipe for streamlining data and flow for function composition (Trust me, this is the final version!).

import * as option from 'fp-ts/Option';
import {pipe, flow} from 'fp-ts/function';

type Address = {
addressLine1: string;
addressLine2?: string;
};

type Person = {
name: string;
age: number;
address?: Address;
};

const getAddressLine2Final = flow(
option.fromNullableK((person: Person) => person.address),
option.chain(address => option.fromNullable(address.addressLine2)),
option.getOrElse(() => '')
);

const personA = {
name: 'A',
age: 20,
address: {
addressLine1: 'hehe',
addressLine2: 'haha',
},
};

// You should get "haha"
pipe(personA, getAddressLine2Final);

Here comes the biggest question:
Why do I want to spend all these efforts to use Option to wrap and unwrap the data just to get an optional value?

Imagine there are more nested level of optional properties in the object. For instances, the type of addressLine2 is no longer a string, but it is changed to

type AddressLine2 = {
country?: {
city?: string;
}
}

When you access the value of city and perform some data transformation in between (such as converting addressLine2 value to lowercase, validating the value of addressLine and city), do you want to check whether the value is empty or not every time by doing:

person.address && person.address.addressLine2 && 
person.address.addressLine2 && person.address.addressLine2.country &&
person.address.addressLine2.country.city

or just adding

option.chain(addressLine2 => option.fromNullable(address.country)),
option.chain(country => option.fromNullable(country.city)),

More articles will be provided with practical examples to show how to play around between different common data structure like Option, Either, TaskEither.

More about fp-ts.

Thanks for reading 🫡
Hope you enjoy this article and gain something from it. I wish it is less daunting now for you to learn Functional Programming :)

Ka Hin

--

--

Michael

Software Engineer🧑🏻‍💻Master in Computer Science🎓. Helping people to learn and use Functional Programming in JS. F# and JS developer🧑🏻‍💻 FP-TS ➡️