Skip to content

Option

The Option type represents optional values and is a replacement for using null or undefined. An Option<A> can either be Some<A>, containing a value of type A, or None, representing the absence of a value.

Passing around an Option<string> is more descriptive than passing around a string | null | undefined, as it clearly indicates that the value may or may not be present.

We might have a function that takes in an optional argument that could also be undefined and we want to do something different if it’s not provided versus when it is provided but not defined.

function greet(name?: string) {
// We have no way of knowing if the name is provided or not!
if(!name) {
return "Hello, stranger!"
} else {
return `Hello, ${name}!`
}
}

null is often used for this case, so now the type for name will be string | null | undefined, which is not very descriptive.

import { expect } from "jsr:@std/expect"
function greet(name?: string | null) {
// name can be undefined, null, or a string, even though we only defined it as string | null
// don't forget to not mix up === and ==!
if (name === null) {
// passing null means that we have a name, it's just null.
return `Hello, anonymous!`
} else if (name === undefined) {
return `Hello, stranger!`
}
return `Hello, ${name}!`
}
expect(greet()).toEqual("Hello, stranger!")
expect(greet(null)).toEqual("Hello, anonymous!")
expect(greet("John")).toEqual("Hello, John!")

Using Option makes it clear that the value may not be present, and we can handle both cases in a more type-safe way.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
// We can use default values for our functions arguments
function greet(name: Option.Option<string> = Option.of("stranger")) {
return pipe(
name,
Option.map((n) => `Hello, ${n}!`),
Option.getOrElse(() => "Hello, anonymous!"),
)
}
expect(greet()).toEqual("Hello, stranger!")
expect(greet(Option.none)).toEqual("Hello, anonymous!")
expect(greet(Option.of("John"))).toEqual("Hello, John!")

An Option can be great to represent keys that are present on an Object vs ones that are not. This allows you to treat undefined as something that truely doesn’t exist, rather than just being a value that is not set or is empty.

An object with undefined values.

import { expect } from "jsr:@std/expect"
const obj: Record<string, string | undefined> = {
name: "John",
age: undefined,
city: "New York",
}
expect(obj["name"]).toEqual("John")
expect(obj["age"]).toEqual(undefined) // this is defined, but has no value
expect(obj["height"]).toEqual(undefined) // this key doesn't exist

An object with Option values.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
const obj: Record<string, Option.Option<string>> = {
name: Option.some("John"),
age: Option.none,
city: Option.some("New York"),
}
expect(obj["name"]).toEqual(Option.some("John"))
expect(obj["age"]).toEqual(Option.none) // this is defined, but has no value
expect(obj["height"]).toEqual(undefined) // this key doesn't exist

This is the main way to create an Option. It’s similar to doing Array.of to create a new array.

Returns None if the value is null or undefined, otherwise wraps the value in a Some.

Creating an option

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.of(undefined)).toEqual({ _tag: "None" })
expect(Option.of(null)).toEqual({ _tag: "None" })
expect(Option.of(1)).toEqual({ _tag: "Some", value: 1 })

Constructs a Some. Represents an optional value that exists. This value cannot be null or undefined.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.some(1)).toEqual({ _tag: "Some", value: 1 })
expect(Option.some("hello")).toEqual({ _tag: "Some", value: "hello" })

None doesn’t have a constructor, instead you can use it directly as a value. Represents a missing value.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.none).toEqual({ _tag: "None" })

You can create an Option based on a predicate, for example, to check if a value is positive.

The fromPredicate function can be a (type predicate function)[https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates], meaning it can the type of the value.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
const isPositive = Option.fromPredicate((n: number): n is number => n >= 0)
expect(isPositive(-1)).toEqual(Option.none)
expect(isPositive(1)).toEqual(Option.some(1))

tryCatch is a utility function that allows you to execute a function that may throw an error and return an Option. You can also provide a function to call when there is an error, which is useful for logging or other side effects you might want to use when there is an error.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.tryCatch(() => 1)).toEqual(Option.some(1))
expect(Option.tryCatch(() => { throw new Error("Error") })).toEqual(Option.none)
expect(Option.tryCatch(() => { throw new Error("Error with logging") }, e => `Error!: ${e}`)).toEqual(Option.none)

The Option.map function lets you transform the value inside an Option without manually unwrapping and re-wrapping it. If the Option holds a value (Some), the transformation function is applied. If the Option is None, the function is ignored, and the Option remains unchanged.

Mapping a Value in Some.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
// Transform the value inside Some
expect(Option.map(Option.some(1), (n: number) => n + 1)).toEqual(Option.some(2))

Mapping over None.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
// Mapping over None results in None
expect(Option.map(Option.none, (n: number) => n + 1)).toEqual(Option.none)

Applies a function to the value of a Some and flattens the resulting Option. If the input is None, it remains None.

This function allows you to chain computations that return Option values. If the input Option is Some, the provided function f is applied to the contained value, and the resulting Option is returned. If the input is None, the function is not applied, and the result remains None.

This utility is particularly useful for sequencing operations that may fail or produce optional results, enabling clean and concise workflows for handling such cases.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
type Address = {
readonly city: string
readonly street: Option.Option<string>
}
type User = {
readonly id: number
readonly username: string
readonly email: Option.Option<string>
readonly address: Option.Option<Address>
}
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")
})
}
// Use flatMap to extract the street value
const street = pipe(
user.address,
Option.flatMap(({ street }) => street)
)
expect(street).toEqual(Option.some("123 Main St"))

forEach applies a side effect to the value inside an Option if it is Some and returns the original Option.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
let counter = 0
const incrementCounter = () => counter++
expect(Option.forEach(Option.some(42), () => incrementCounter())).toEqual(Option.some(42))
expect(counter).toEqual(1)
expect(Option.forEach(Option.none, () => incrementCounter())).toEqual(Option.none)
expect(counter).toEqual(1)

Applies a filter function to an Option, returning the Option itself if the value satisfies the predicate, or None if it does not.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
expect(Option.filter(Option.some(42), (n) => n > 40)).toEqual(Option.some(42))
expect(Option.filter(Option.some(42), (n) => n < 40)).toEqual(Option.none)
expect(
pipe(
Option.some(42),
Option.filter((n) => n > 40),
)
).toEqual(Option.some(42))
expect(
pipe(
Option.some(42),
Option.filter((n) => n < 40),
)
).toEqual(Option.none)

Matches an Option against two functions: one for the Some case and one for the None case. This is useful for handling both cases in a single expression without needing to check the _tag manually.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
expect(Option.match(Option.some(42), () => "There is no value.", val => `The value is ${val}.`)).toEqual("The value is 42.")
expect(Option.match(Option.none, () => "There is no value.", val => `The value is ${val}.`)).toEqual("There is no value.")
expect(
pipe(
Option.some(42),
Option.match(
() => "There is no value.",
(val) => `The value is ${val}.`
)
)
).toEqual("The value is 42.")
expect(
pipe(
Option.none,
Option.match(
() => "There is no value.",
(val) => `The value is ${val}.`
)
)
).toEqual("There is no value.")

Gets the value from an Option, or returns a fallback value if the Option is None.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
expect(Option.getOrElse(Option.some(42), () => 10)).toEqual(42)
expect(Option.getOrElse(Option.none, () => 10)).toEqual(10)
expect(
pipe(
Option.some(42),
Option.getOrElse(() => 10),
)
).toEqual(42)
expect(
pipe(
Option.none,
Option.getOrElse(() => 10),
)
).toEqual(10)

Provides an alternative Option value if the current Option is None. If the current Option is Some, it returns the current value. If it’s None, it returns the result of evaluating the provided function.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
expect(Option.alt(Option.some(1), () => Option.some(2))).toEqual(Option.some(1))
expect(Option.alt(Option.none, () => Option.some(2))).toEqual(Option.some(2))
// Using pipe
expect(
pipe(
Option.some(1),
Option.alt(() => Option.some(2))
)
).toEqual(Option.some(1))
expect(
pipe(
Option.none,
Option.alt(() => Option.some(2))
)
).toEqual(Option.some(2))

Checks if an Option is a Some value. This works as a valid type guard, allowing TypeScript to narrow the type of the Option to Some<T> when this function returns true.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.isSome(Option.some(1))).toEqual(true)
expect(Option.isSome(Option.none)).toEqual(false)

Checks if an Option is a None value. This works as a valid type guard, allowing TypeScript to narrow the type of the Option to None when this function returns true.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.isNone(Option.some(1))).toEqual(false)
expect(Option.isNone(Option.none)).toEqual(true)

Converts an Either into an Option, mapping the Right value to Some and the Left value to None.

import { expect } from "jsr:@std/expect"
import { Option, Either } from "@jvlk/fp-tsm"
expect(Option.fromEither(Either.right(1))).toEqual(Option.some(1))
expect(Option.fromEither(Either.left("error"))).toEqual(Option.none)

Converts an Option to a nullable value. If the Option is Some, it returns the contained value; if it is None, it returns null. This should be used only when you are doing interop with other libraries that expect null values. If you are using Option as a type, you should prefer to use None instead of null.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.toNullable(Option.some(1))).toEqual(1)
expect(Option.toNullable(Option.none)).toEqual(null)

Converts an Option to an undefined value. If the Option is Some, it returns the contained value; if it is None, it returns undefined. This should be used only when you are doing interop with other libraries that expect undefined values. If you are using Option as a type, you should prefer to use None instead of undefined.

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
expect(Option.toUndefined(Option.some(1))).toEqual(1)
expect(Option.toUndefined(Option.none)).toEqual(undefined)

Do notation allows you to yield Option values and combine them in a sequential manner without having to manually check for None at each step.

The yield* operator is used to work with multiple Option values in a generator function. Each value must be yielded with Option.bind().

import { expect } from "jsr:@std/expect"
import { Option } from "@jvlk/fp-tsm"
const age = Option.some(30)
const name = Option.some("John")
const city = Option.some("New York")
const data = Option.Do(function* () {
const personAge = yield* Option.bind(age)
const personName = yield* Option.bind(name)
const personCity = yield* Option.bind(city)
return `Hello ${personName}! You are ${personAge} years old and live in ${personCity}.`
})
expect(data).toEqual(Option.some("Hello John! You are 30 years old and live in New York."))
// If any Option is None, the entire result is None
const data2 = Option.Do(function* () {
const personAge = yield* Option.bind(Option.none)
const personName = yield* Option.bind(name)
return `Hello ${personName}! You are ${personAge} years old.`
})
expect(data2).toEqual(Option.none)

Without Do notation, the same code would be much more verbose.

import { expect } from "jsr:@std/expect"
import { Option, pipe } from "@jvlk/fp-tsm"
const age = Option.some(30)
const name = Option.some("John")
const city = Option.some("New York")
const result = pipe(
age,
Option.flatMap(personAge =>
pipe(
name,
Option.flatMap(personName =>
pipe(
city,
Option.map(personCity =>
`Hello ${personName}! You are ${personAge} years old and live in ${personCity}.`
)
)
)
)
)
)
expect(result).toEqual(Option.some("Hello John! You are 30 years old and live in New York."))