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"
 
const value = Option.some(1) // An Option holding the number 1
ts
import { Option } from "effect"
 
const value = 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"
 
const noValue = Option.none() // An Option holding no value
ts
import { Option } from "effect"
 
const noValue = 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"
 
interface User {
readonly id: number
readonly username: string
readonly email: Option.Option<string>
}
ts
import { Option } from "effect"
 
interface User {
readonly id: number
readonly username: string
readonly email: Option.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
const withEmail: User = {
id: 1,
username: "john_doe",
email: Option.some("john.doe@example.com")
}
 
const withoutEmail: User = {
id: 2,
username: "jane_doe",
email: Option.none()
}
ts
const withEmail: User = {
id: 1,
username: "john_doe",
email: Option.some("john.doe@example.com")
}
 
const withoutEmail: User = {
id: 2,
username: "jane_doe",
email: 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"
 
const foo = Option.some(1)
 
console.log(Option.isSome(foo)) // Output: true
 
if (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"
 
const foo = Option.some(1)
 
console.log(Option.isSome(foo)) // Output: true
 
if (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"
 
const foo = Option.some(1)
 
const result = 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"
 
const foo = Option.some(1)
 
const result = 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"
 
const maybeIncremented = Option.map(Option.some(1), (n) => n + 1) // some(2)
ts
import { Option } from "effect"
 
const maybeIncremented = 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"
 
const maybeIncremented = Option.map(Option.none(), (n) => n + 1) // none()
ts
import { Option } from "effect"
 
const maybeIncremented = 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"
 
interface User {
readonly id: number
readonly username: string
readonly email: Option.Option<string>
readonly address: Option.Option<Address>
}
ts
import { Option } from "effect"
 
interface User {
readonly id: number
readonly username: string
readonly email: Option.Option<string>
readonly address: Option.Option<Address>
}

The address field itself contains a nested optional property called street of type Option<string>:

ts
interface Address {
readonly city: string
readonly street: Option.Option<string>
}
ts
interface Address {
readonly city: string
readonly street: Option.Option<string>
}

We can use Option.flatMap to extract the street property from the address field.

ts
const user: User = {
id: 1,
username: "john_doe",
email: Option.some("john.doe@example.com"),
address: Option.some({
city: "New York",
street: Option.some("123 Main St")
})
}
 
const street = user.address.pipe(Option.flatMap((address) => address.street))
ts
const user: User = {
id: 1,
username: "john_doe",
email: Option.some("john.doe@example.com"),
address: Option.some({
city: "New York",
street: Option.some("123 Main St")
})
}
 
const street = 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 an Option, or throws an error if the Option is a None. Here's an example:

    ts
    import { Option } from "effect"
     
    Option.getOrThrow(Option.some(10)) // 10
    Option.getOrThrow(Option.none()) // throws getOrThrow called on a None
    ts
    import { Option } from "effect"
     
    Option.getOrThrow(Option.some(10)) // 10
    Option.getOrThrow(Option.none()) // throws getOrThrow called on a None
  • getOrNull and getOrUndefined: These functions are useful when you want to work with code that doesn't use Option. They allow you to retrieve the value of an Option as null or undefined, respectively. Examples:

    ts
    import { Option } from "effect"
     
    Option.getOrNull(Option.some(5)) // 5
    Option.getOrNull(Option.none()) // null
     
    Option.getOrUndefined(Option.some(5)) // 5
    Option.getOrUndefined(Option.none()) // undefined
    ts
    import { Option } from "effect"
     
    Option.getOrNull(Option.some(5)) // 5
    Option.getOrNull(Option.none()) // null
     
    Option.getOrUndefined(Option.some(5)) // 5
    Option.getOrUndefined(Option.none()) // undefined
  • getOrElse: This function lets you provide a default value that will be returned if the Option is a None. Here's an example:

    ts
    import { Option } from "effect"
     
    Option.getOrElse(Option.some(5), () => 0) // 5
    Option.getOrElse(Option.none(), () => 0) // 0
    ts
    import { Option } from "effect"
     
    Option.getOrElse(Option.some(5), () => 0) // 5
    Option.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 result
const performComputation = (): Option.Option<number> =>
Math.random() < 0.5 ? Option.some(10) : Option.none()
 
const performAlternativeComputation = (): Option.Option<number> =>
Math.random() < 0.5 ? Option.some(20) : Option.none()
 
const result = 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 result
const performComputation = (): Option.Option<number> =>
Math.random() < 0.5 ? Option.some(10) : Option.none()
 
const performAlternativeComputation = (): Option.Option<number> =>
Math.random() < 0.5 ? Option.some(20) : Option.none()
 
const result = 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"
 
const first = Option.firstSomeOf([
Option.none(),
Option.some(2),
Option.none(),
Option.some(3)
]) // some(2)
ts
import { Option } from "effect"
 
const first = 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 to null using the getOrNull API.
  • Convert None to undefined using the getOrUndefined API.
ts
import { Option } from "effect"
 
Option.getOrNull(Option.some(5)) // 5
Option.getOrNull(Option.none()) // null
 
Option.getOrUndefined(Option.some(5)) // 5
Option.getOrUndefined(Option.none()) // undefined
ts
import { Option } from "effect"
 
Option.getOrNull(Option.some(5)) // 5
Option.getOrNull(Option.none()) // null
 
Option.getOrUndefined(Option.some(5)) // 5
Option.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 to Effect<never, NoSuchElementException>
  • Some<A> is equivalent to Effect<A>

To illustrate this interoperability, let's consider the following example:

ts
import { Effect, Option } from "effect"
 
const head = <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: 1
 
Effect.runSync(Effect.succeed([]).pipe(Effect.flatMap(head))) // throws NoSuchElementException: undefined
ts
import { Effect, Option } from "effect"
 
const head = <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: 1
 
Effect.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"
 
const maybeName = Option.some("John")
const maybeAge = Option.some(25)
 
const person = 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"
 
const maybeName = Option.some("John")
const maybeAge = Option.some(25)
 
const person = 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"
 
const maybeName = Option.some("John")
const maybeAge = Option.none()
 
const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({
name,
age
}))
 
console.log(person)
/*
Output:
{
_id: "Option",
_tag: "None"
}
*/
ts
import { Option } from "effect"
 
const maybeName = Option.some("John")
const maybeAge = Option.none()
 
const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({
name,
age
}))
 
console.log(person)
/*
Output:
{
_id: "Option",
_tag: "None"
}
*/