Option
On this page
The Option
data type is used to represent optional values. An Option
can be either Some
, which contains a value, or None
, which indicates the absence of a value.
The Option
type is versatile and can be applied in various scenarios, including:
- Using it for initial values
- Returning values from functions that are not defined for all possible inputs (referred to as "partial functions")
- Managing optional fields in data structures
- Handling optional function arguments
Creating Options
The some
constructor takes a value of type A
and returns an Option<A>
that holds that value:
ts
import {Option } from "effect"constvalue =Option .some (1) // An Option holding the number 1
ts
import {Option } from "effect"constvalue =Option .some (1) // An Option holding the number 1
On the other hand, the none
constructor returns an Option<never>
, representing the absence of a value:
ts
import {Option } from "effect"constnoValue =Option .none () // An Option holding no value
ts
import {Option } from "effect"constnoValue =Option .none () // An Option holding no value
Modeling Optional Properties
Let's look at an example of a User
model where the "email"
property is optional and can have a value of type string
.
To represent this, we can use the Option<string>
type:
ts
import {Option } from "effect"interfaceUser {readonlyid : numberreadonlyusername : stringreadonlyOption .Option <string>}
ts
import {Option } from "effect"interfaceUser {readonlyid : numberreadonlyusername : stringreadonlyOption .Option <string>}
Optionality only applies to the value of the property. The key "email"
will always be present in the object, regardless of whether it has a value
or not.
Now, let's see how we can create instances of User
with and without an email:
ts
constwithEmail :User = {id : 1,username : "john_doe",Option .some ("john.doe@example.com")}constwithoutEmail :User = {id : 2,username : "jane_doe",Option .none ()}
ts
constwithEmail :User = {id : 1,username : "john_doe",Option .some ("john.doe@example.com")}constwithoutEmail :User = {id : 2,username : "jane_doe",Option .none ()}
Guards
You can determine whether an Option
is a Some
or a None
by using the isSome
and isNone
guards:
ts
import {Option } from "effect"constfoo =Option .some (1)console .log (Option .isSome (foo )) // Output: trueif (Option .isNone (foo )) {console .log ("Option is empty")} else {console .log (`Option has a value: ${foo .value }`)}// Output: "Option has a value: 1"
ts
import {Option } from "effect"constfoo =Option .some (1)console .log (Option .isSome (foo )) // Output: trueif (Option .isNone (foo )) {console .log ("Option is empty")} else {console .log (`Option has a value: ${foo .value }`)}// Output: "Option has a value: 1"
Matching
The Option.match
function allows you to handle different cases of an Option
value by providing separate actions for each case:
ts
import {Option } from "effect"constfoo =Option .some (1)constresult =Option .match (foo , {onNone : () => "Option is empty",onSome : (value ) => `Option has a value: ${value }`})console .log (result ) // Output: "Option has a value: 1"
ts
import {Option } from "effect"constfoo =Option .some (1)constresult =Option .match (foo , {onNone : () => "Option is empty",onSome : (value ) => `Option has a value: ${value }`})console .log (result ) // Output: "Option has a value: 1"
Using match
instead of isSome
or isNone
can be more expressive and
provide a clear way to handle both cases of an Option
.
Working with Option
The Option.map
function allows you to transform the value inside an Option
without having to unwrap and wrap the underlying value. Let's see an example:
ts
import {Option } from "effect"constmaybeIncremented =Option .map (Option .some (1), (n ) =>n + 1) // some(2)
ts
import {Option } from "effect"constmaybeIncremented =Option .map (Option .some (1), (n ) =>n + 1) // some(2)
The convenient aspect of using Option
is how it handles the absence of a value, represented by None
:
ts
import {Option } from "effect"constmaybeIncremented =Option .map (Option .none (), (n ) =>n + 1) // none()
ts
import {Option } from "effect"constmaybeIncremented =Option .map (Option .none (), (n ) =>n + 1) // none()
Despite having None
as the input, we can still operate on the Option
without encountering errors. The mapping function (n) => n + 1
is not executed when the Option
is None
, and the result remains none
representing the absence of a value.
The flatMap
function works similarly to map
, but with an additional feature. It allows us to sequence computations that depend on the absence or presence of a value in an Option
.
Let's explore an example that involves a nested optional property. We have a User
model with an optional address
field of type Option<Address>
:
ts
import {Option } from "effect"interfaceUser {readonlyid : numberreadonlyusername : stringreadonlyOption .Option <string>readonlyaddress :Option .Option <Address >}
ts
import {Option } from "effect"interfaceUser {readonlyid : numberreadonlyusername : stringreadonlyOption .Option <string>readonlyaddress :Option .Option <Address >}
The address
field itself contains a nested optional property called street
of type Option<string>
:
ts
interfaceAddress {readonlycity : stringreadonlystreet :Option .Option <string>}
ts
interfaceAddress {readonlycity : stringreadonlystreet :Option .Option <string>}
We can use Option.flatMap
to extract the street
property from the address
field.
ts
constuser :User = {id : 1,username : "john_doe",Option .some ("john.doe@example.com"),address :Option .some ({city : "New York",street :Option .some ("123 Main St")})}conststreet =user .address .pipe (Option .flatMap ((address ) =>address .street ))
ts
constuser :User = {id : 1,username : "john_doe",Option .some ("john.doe@example.com"),address :Option .some ({city : "New York",street :Option .some ("123 Main St")})}conststreet =user .address .pipe (Option .flatMap ((address ) =>address .street ))
Here's how it works: if the address
is Some
, meaning it has a value, the mapping function (addr) => addr.street
is applied to retrieve the street
value. On the other hand, if the address
is None
, indicating the absence of a value, the mapping function is not executed, and the result is also None
.
Here's a summary of the two functions:
Getting the Value from an Option
To retrieve the value stored within an Option
, you can use various functions provided by the Option
module. Let's explore these functions:
-
getOrThrow
: It retrieves the wrapped value from anOption
, or throws an error if theOption
is aNone
. Here's an example:tsimport {Option } from "effect"Option .getOrThrow (Option .some (10)) // 10Option .getOrThrow (Option .none ()) // throws getOrThrow called on a Nonetsimport {Option } from "effect"Option .getOrThrow (Option .some (10)) // 10Option .getOrThrow (Option .none ()) // throws getOrThrow called on a None -
getOrNull
andgetOrUndefined
: These functions are useful when you want to work with code that doesn't useOption
. They allow you to retrieve the value of anOption
asnull
orundefined
, respectively. Examples:tsimport {Option } from "effect"Option .getOrNull (Option .some (5)) // 5Option .getOrNull (Option .none ()) // nullOption .getOrUndefined (Option .some (5)) // 5Option .getOrUndefined (Option .none ()) // undefinedtsimport {Option } from "effect"Option .getOrNull (Option .some (5)) // 5Option .getOrNull (Option .none ()) // nullOption .getOrUndefined (Option .some (5)) // 5Option .getOrUndefined (Option .none ()) // undefined -
getOrElse
: This function lets you provide a default value that will be returned if theOption
is aNone
. Here's an example:tsimport {Option } from "effect"Option .getOrElse (Option .some (5), () => 0) // 5Option .getOrElse (Option .none (), () => 0) // 0tsimport {Option } from "effect"Option .getOrElse (Option .some (5), () => 0) // 5Option .getOrElse (Option .none (), () => 0) // 0
Fallback
In certain situations, when a computation returns None
, you may want to try an alternative computation that returns an Option
. This is where the Option.orElse
function comes in handy. It allows you to chain multiple computations together and continue with the next one if the previous one resulted in None
. This can be useful for implementing retry logic, where you want to attempt a computation multiple times until you either succeed or exhaust all possible attempts.
ts
import {Option } from "effect"// Simulating a computation that may or may not produce a resultconstperformComputation = ():Option .Option <number> =>Math .random () < 0.5 ?Option .some (10) :Option .none ()constperformAlternativeComputation = ():Option .Option <number> =>Math .random () < 0.5 ?Option .some (20) :Option .none ()constresult =performComputation ().pipe (Option .orElse (() =>performAlternativeComputation ()))Option .match (result , {onNone : () =>console .log ("Both computations resulted in None"),onSome : (value ) =>console .log ("Computed value:",value ) // At least one computation succeeded})
ts
import {Option } from "effect"// Simulating a computation that may or may not produce a resultconstperformComputation = ():Option .Option <number> =>Math .random () < 0.5 ?Option .some (10) :Option .none ()constperformAlternativeComputation = ():Option .Option <number> =>Math .random () < 0.5 ?Option .some (20) :Option .none ()constresult =performComputation ().pipe (Option .orElse (() =>performAlternativeComputation ()))Option .match (result , {onNone : () =>console .log ("Both computations resulted in None"),onSome : (value ) =>console .log ("Computed value:",value ) // At least one computation succeeded})
Additionally, the Option.firstSomeOf
function can be used to retrieve the first value that is Some
within an iterable of Option
values:
ts
import {Option } from "effect"constfirst =Option .firstSomeOf ([Option .none (),Option .some (2),Option .none (),Option .some (3)]) // some(2)
ts
import {Option } from "effect"constfirst =Option .firstSomeOf ([Option .none (),Option .some (2),Option .none (),Option .some (3)]) // some(2)
Interop with Nullable Types
When working with the Option
data type, you may come across code that uses undefined
or null
to represent optional values. The Option
data type provides several APIs to facilitate the interaction between Option
and nullable types.
You can create an Option
from a nullable value using the fromNullable
API.
ts
import {Option } from "effect"Option .fromNullable (null) // none()Option .fromNullable (undefined ) // none()Option .fromNullable (1) // some(1)
ts
import {Option } from "effect"Option .fromNullable (null) // none()Option .fromNullable (undefined ) // none()Option .fromNullable (1) // some(1)
Conversely, if you have a value of type Option
and want to convert it to a nullable value, you have two options:
- Convert
None
tonull
using thegetOrNull
API. - Convert
None
toundefined
using thegetOrUndefined
API.
ts
import {Option } from "effect"Option .getOrNull (Option .some (5)) // 5Option .getOrNull (Option .none ()) // nullOption .getOrUndefined (Option .some (5)) // 5Option .getOrUndefined (Option .none ()) // undefined
ts
import {Option } from "effect"Option .getOrNull (Option .some (5)) // 5Option .getOrNull (Option .none ()) // nullOption .getOrUndefined (Option .some (5)) // 5Option .getOrUndefined (Option .none ()) // undefined
Interop with Effect
The Option
type is a subtype of the Effect
type, which means that it can be seamlessly used with functions from the Effect
module. These functions are primarily designed to work with Effect
values, but they can also handle Option
values and process them correctly.
In the context of Effect
, the two members of the Option
type are treated as follows:
None
is equivalent toEffect<never, NoSuchElementException>
Some<A>
is equivalent toEffect<A>
To illustrate this interoperability, let's consider the following example:
ts
import {Effect ,Option } from "effect"consthead = <A >(as :ReadonlyArray <A >):Option .Option <A > =>as .length > 0 ?Option .some (as [0]) :Option .none ()console .log (Effect .runSync (Effect .succeed ([1, 2, 3]).pipe (Effect .flatMap (head )))) // Output: 1Effect .runSync (Effect .succeed ([]).pipe (Effect .flatMap (head ))) // throws NoSuchElementException: undefined
ts
import {Effect ,Option } from "effect"consthead = <A >(as :ReadonlyArray <A >):Option .Option <A > =>as .length > 0 ?Option .some (as [0]) :Option .none ()console .log (Effect .runSync (Effect .succeed ([1, 2, 3]).pipe (Effect .flatMap (head )))) // Output: 1Effect .runSync (Effect .succeed ([]).pipe (Effect .flatMap (head ))) // throws NoSuchElementException: undefined
Combining Two or More Options
The Option.zipWith
function allows you to combine two Option
values using a provided function. It creates a new Option
that holds the combined value of both original Option
values.
ts
import {Option } from "effect"constmaybeName =Option .some ("John")constmaybeAge =Option .some (25)constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name ,age }))console .log (person )/*Output:{_id: "Option",_tag: "Some",value: {name: "John",age: 25}}*/
ts
import {Option } from "effect"constmaybeName =Option .some ("John")constmaybeAge =Option .some (25)constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name ,age }))console .log (person )/*Output:{_id: "Option",_tag: "Some",value: {name: "John",age: 25}}*/
The zipWith
function takes three arguments:
- The first
Option
you want to combine - The second
Option
you want to combine - A function that takes two arguments, which are the values held by the two
Options
, and returns the combined value
It's important to note that if either of the two Option
values is None
, the resulting Option
will also be None
:
ts
import {Option } from "effect"constmaybeName =Option .some ("John")constmaybeAge =Option .none ()constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name ,age }))console .log (person )/*Output:{_id: "Option",_tag: "None"}*/
ts
import {Option } from "effect"constmaybeName =Option .some ("John")constmaybeAge =Option .none ()constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name ,age }))console .log (person )/*Output:{_id: "Option",_tag: "None"}*/