(fp-ts series 1: Container)
- 👇🏻 Option
- Either
- TaskEither (part 1) — Basic introduction
- TaskEither (part 2) — Dependent Promises
- TaskEither (part 3) — Independent Promises
- 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 .map
function 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