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
some
The Option.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
none
On the other hand, the Option.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
liftPredicate
Sometimes you need to create an Option
based on a predicate, such as checking if a value is positive.
Here's how you can do this explicitly using Option.none
and Option.some
ts
import {Option } from "effect"constisPositive = (n : number) =>n > 0constparsePositive = (n : number):Option .Option <number> =>isPositive (n ) ?Option .some (n ) :Option .none ()
ts
import {Option } from "effect"constisPositive = (n : number) =>n > 0constparsePositive = (n : number):Option .Option <number> =>isPositive (n ) ?Option .some (n ) :Option .none ()
The same result can be achieved more concisely using Option.liftPredicate
ts
import {Option } from "effect"constisPositive = (n : number) =>n > 0constparsePositive =Option .liftPredicate (isPositive )
ts
import {Option } from "effect"constisPositive = (n : number) =>n > 0constparsePositive =Option .liftPredicate (isPositive )
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",}constwithoutEmail :User = {id : 2,username : "jane_doe",Option .none ()}
ts
constwithEmail :User = {id : 1,username : "john_doe",}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
map
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.
flatMap
The Option.flatMap
function works similarly to Option.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",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",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
.
filter
The Option.filter
function is used to filter an Option
using a predicate. If the predicate is not satisfied or the Option
is None
, it returns None
.
Let's see an example where we refactor some code to a more idiomatic version:
Original Code
ts
import {Option } from "effect"constremoveEmptyString = (input :Option .Option <string>) => {if (Option .isSome (input ) &&input .value === "") {returnOption .none ()}returninput }console .log (removeEmptyString (Option .none ())) // { _id: 'Option', _tag: 'None' }console .log (removeEmptyString (Option .some (""))) // { _id: 'Option', _tag: 'None' }console .log (removeEmptyString (Option .some ("a"))) // { _id: 'Option', _tag: 'Some', value: 'a' }
ts
import {Option } from "effect"constremoveEmptyString = (input :Option .Option <string>) => {if (Option .isSome (input ) &&input .value === "") {returnOption .none ()}returninput }console .log (removeEmptyString (Option .none ())) // { _id: 'Option', _tag: 'None' }console .log (removeEmptyString (Option .some (""))) // { _id: 'Option', _tag: 'None' }console .log (removeEmptyString (Option .some ("a"))) // { _id: 'Option', _tag: 'Some', value: 'a' }
Idiomatic Code
ts
import {Option } from "effect"constremoveEmptyString = (input :Option .Option <string>) =>Option .filter (input , (value ) =>value !== "")
ts
import {Option } from "effect"constremoveEmptyString = (input :Option .Option <string>) =>Option .filter (input , (value ) =>value !== "")
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 .andThen (head )))) // Output: 1Effect .runSync (Effect .succeed ([]).pipe (Effect .andThen (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 .andThen (head )))) // Output: 1Effect .runSync (Effect .succeed ([]).pipe (Effect .andThen (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 .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name :name .toUpperCase (),age }))console .log (person )/*Output:{ _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } }*/
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name :name .toUpperCase (),age }))console .log (person )/*Output:{ _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } }*/
The Option.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 .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .none ()constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name :name .toUpperCase (),age }))console .log (person )/*Output:{ _id: 'Option', _tag: 'None' }*/
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .none ()constperson =Option .zipWith (maybeName ,maybeAge , (name ,age ) => ({name :name .toUpperCase (),age }))console .log (person )/*Output:{ _id: 'Option', _tag: 'None' }*/
If you need to combine two or more Option
s without transforming the values they hold, you can use Option.all
, which takes a collection of Option
s and returns an Option
with the same structure.
- If a tuple is provided, the returned
Option
will contain a tuple with the same length. - If a struct is provided, the returned
Option
will contain a struct with the same keys. - If an iterable is provided, the returned
Option
will contain an array.
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)consttuple =Option .all ([maybeName ,maybeAge ])conststruct =Option .all ({name :maybeName ,age :maybeAge })
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)consttuple =Option .all ([maybeName ,maybeAge ])conststruct =Option .all ({name :maybeName ,age :maybeAge })
gen
Similar to Effect.gen, there's also Option.gen
, which provides a convenient syntax, akin to async/await, for writing code involving Option
and using generators.
Let's revisit the previous example, this time using Option.gen
instead of Option.zipWith
:
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)constperson =Option .gen (function* () {constname = (yield*maybeName ).toUpperCase ()constage = yield*maybeAge return {name ,age }})console .log (person )/*Output:{ _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } }*/
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .some (25)constperson =Option .gen (function* () {constname = (yield*maybeName ).toUpperCase ()constage = yield*maybeAge return {name ,age }})console .log (person )/*Output:{ _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } }*/
Once again, if either of the two Option
values is None
, the resulting Option
will also be None
:
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .none ()constperson =Option .gen (function* () {constname = (yield*maybeName ).toUpperCase ()constage = yield*maybeAge return {name ,age }})console .log (person )/*Output:{ _id: 'Option', _tag: 'None' }*/
ts
import {Option } from "effect"constmaybeName :Option .Option <string> =Option .some ("John")constmaybeAge :Option .Option <number> =Option .none ()constperson =Option .gen (function* () {constname = (yield*maybeName ).toUpperCase ()constage = yield*maybeAge return {name ,age }})console .log (person )/*Output:{ _id: 'Option', _tag: 'None' }*/
Comparing Option Values with Equivalence
You can compare Option
values using the Option.getEquivalence
function.
This function lets you define rules for comparing the contents of Option
types by providing an Equivalence
for the type of value they might contain.
Example: Checking Equivalence of Optional Numbers
Imagine you have optional numbers and you want to check if they are equivalent. Here’s how you can do it:
ts
import {Option ,Equivalence } from "effect"constmyEquivalence =Option .getEquivalence (Equivalence .number )console .log (myEquivalence (Option .some (1),Option .some (1))) // Output: true, because both options contain the number 1console .log (myEquivalence (Option .some (1),Option .some (2))) // Output: false, because the numbers are differentconsole .log (myEquivalence (Option .some (1),Option .none ())) // Output: false, because one is a number and the other is empty
ts
import {Option ,Equivalence } from "effect"constmyEquivalence =Option .getEquivalence (Equivalence .number )console .log (myEquivalence (Option .some (1),Option .some (1))) // Output: true, because both options contain the number 1console .log (myEquivalence (Option .some (1),Option .some (2))) // Output: false, because the numbers are differentconsole .log (myEquivalence (Option .some (1),Option .none ())) // Output: false, because one is a number and the other is empty
Sorting Option Values with Order
Sorting a collection of Option
values can be done using the Option.getOrder
function.
This function helps you sort Option
values by providing a custom sorting rule for the type of value they might contain.
Example: Sorting Optional Numbers
Suppose you have a list of optional numbers and you want to sort them in ascending order, considering empty values as the lowest:
ts
import {Option ,Array ,Order } from "effect"constitems = [Option .some (1),Option .none (),Option .some (2)]constmyOrder =Option .getOrder (Order .number )console .log (Array .sort (myOrder )(items ))/*Output:[{ _id: 'Option', _tag: 'None' }, // None appears first because it's considered the lowest{ _id: 'Option', _tag: 'Some', value: 1 }, // Sorted in ascending order{ _id: 'Option', _tag: 'Some', value: 2 }]*/
ts
import {Option ,Array ,Order } from "effect"constitems = [Option .some (1),Option .none (),Option .some (2)]constmyOrder =Option .getOrder (Order .number )console .log (Array .sort (myOrder )(items ))/*Output:[{ _id: 'Option', _tag: 'None' }, // None appears first because it's considered the lowest{ _id: 'Option', _tag: 'Some', value: 1 }, // Sorted in ascending order{ _id: 'Option', _tag: 'Some', value: 2 }]*/
In this example, Option.none()
is treated as the lowest value, allowing Option.some(1)
and Option.some(2)
to be sorted in ascending order based on their numerical value. This method ensures that all Option
values are sorted logically according to their content, with empty values (Option.none()
) being placed before non-empty values (Option.some()
).
Advanced Example: Sorting Optional Dates in Reverse Order
Now, let's consider a more complex scenario where you have a list of objects containing optional dates, and you want to sort them in descending order, with any empty optional values placed at the end:
ts
import { Option, Array, Order } from "effect"const items = [{ data: Option.some(new Date(10)) },{ data: Option.some(new Date(20)) },{ data: Option.none() }]// Define the order to sort dates within Option values in reverseconst sorted = Array.sortWith(items,item => item.data,Order.reverse(Option.getOrder(Order.Date)))console.log(sorted)/*Output:[{ data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.020Z' } },{ data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.010Z' } },{ data: { _id: 'Option', _tag: 'None' } } // None placed last]*/
ts
import { Option, Array, Order } from "effect"const items = [{ data: Option.some(new Date(10)) },{ data: Option.some(new Date(20)) },{ data: Option.none() }]// Define the order to sort dates within Option values in reverseconst sorted = Array.sortWith(items,item => item.data,Order.reverse(Option.getOrder(Order.Date)))console.log(sorted)/*Output:[{ data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.020Z' } },{ data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.010Z' } },{ data: { _id: 'Option', _tag: 'None' } } // None placed last]*/