Skip to content

Option

Option<A> models a value that may be absent. It is either Some<A>, holding a value of type A, or None, representing no value. Use it instead of null/undefined when absence is a normal, expected outcome — looking up a key, parsing input, reading an optional field. The type then makes the missing case visible and the compiler forces you to handle it.

import { Option } from "effect"
// A partial function: defined only for non-empty arrays
const head = <A>(array: ReadonlyArray<A>): Option.Option<A> =>
array.length > 0 ? Option.some(array[0]) : Option.none()
// Handle both cases explicitly — no `undefined` checks leaking downstream
const describe = <A>(array: ReadonlyArray<A>): string =>
Option.match(head(array), {
onNone: () => "the array is empty",
onSome: (value) => `the first element is ${value}`
})
console.log(describe([1, 2, 3])) // "the first element is 1"
console.log(describe([])) // "the array is empty"

head returns an Option rather than A | undefined, so every caller must decide what to do when the value is missing. Option.match is the safe way to do that: it takes one callback per case and returns a single value.

import { Option } from "effect"
const value = Option.some(1) // { _id: 'Option', _tag: 'Some', value: 1 }
const empty = Option.none() // { _id: 'Option', _tag: 'None' }

Option.none() is a function call (not a constant) so it can infer the element type at the use site.

To build an Option from a predicate, use Option.liftPredicate:

import { Option } from "effect"
// ┌─── (n: number) => Option<number>
// ▼
const parsePositive = Option.liftPredicate((n: number) => n > 0)
console.log(parsePositive(10)) // { _id: 'Option', _tag: 'Some', value: 10 }
console.log(parsePositive(-1)) // { _id: 'Option', _tag: 'None' }

Use Option<A> for properties that may have no value. The key is always present; only the value is optional, which is exactly what Option expresses.

import { Option } from "effect"
interface User {
readonly id: number
readonly username: string
// The `email` key is always present; the value may be absent
readonly email: Option.Option<string>
}
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()
}

Option.map transforms the value inside a Some and leaves a None untouched. Option.flatMap is for functions that themselves return an Option, letting you walk nested optional data without manual checks.

import { Option } from "effect"
interface Address {
readonly city: string
readonly street: Option.Option<string>
}
interface User {
readonly address: Option.Option<Address>
}
const user: User = {
address: Option.some({
city: "New York",
street: Option.some("123 Main St")
})
}
// If `address` is None, the chain short-circuits to None.
// Otherwise we dig into the nested optional `street`.
const street = user.address.pipe(
Option.flatMap((address) => address.street)
)
console.log(street) // { _id: 'Option', _tag: 'Some', value: '123 Main St' }

Option.filter keeps the value only if a predicate holds, turning it into None otherwise:

import { Option } from "effect"
// Treat an empty string as "no value"
const nonEmpty = (input: Option.Option<string>) =>
Option.filter(input, (value) => value !== "")
console.log(nonEmpty(Option.some(""))) // { _id: 'Option', _tag: 'None' }
console.log(nonEmpty(Option.some("a"))) // { _id: 'Option', _tag: 'Some', value: 'a' }

To get a plain value back out, supply a fallback so the None case is always handled:

import { Option } from "effect"
// Provide a default for the None case
console.log(Option.getOrElse(Option.some(5), () => 0)) // 5
console.log(Option.getOrElse(Option.none(), () => 0)) // 0
// Interop with code that expects null / undefined
console.log(Option.getOrNull(Option.none())) // null
console.log(Option.getOrUndefined(Option.some(5))) // 5
// Throws if None — use only at boundaries where None is truly impossible
console.log(Option.getOrThrow(Option.some(10))) // 10

Option.orElse tries an alternative when the first Option is None, and Option.firstSomeOf returns the first Some from a list:

import { Option } from "effect"
const fromCache = Option.none<number>()
const fromDb = Option.some(42)
console.log(Option.orElse(fromCache, () => fromDb))
// { _id: 'Option', _tag: 'Some', value: 42 }
console.log(
Option.firstSomeOf([Option.none(), Option.some(2), Option.some(3)])
)
// { _id: 'Option', _tag: 'Some', value: 2 }

Option.fromNullishOr converts null/undefined into None, and any other value into Some. It is the bridge between Effect code and APIs that return nullable values.

import { Option } from "effect"
console.log(Option.fromNullishOr(null)) // { _id: 'Option', _tag: 'None' }
console.log(Option.fromNullishOr(undefined)) // { _id: 'Option', _tag: 'None' }
console.log(Option.fromNullishOr(1)) // { _id: 'Option', _tag: 'Some', value: 1 }

Going the other way, Option.getOrNull / Option.getOrUndefined turn a None back into null / undefined.

Option.all combines several Options into one, preserving the input shape (tuple, struct, or iterable). If any input is None, the result is None.

import { Option } from "effect"
const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.some(25)
// ┌─── Option<{ name: string; age: number }>
// ▼
const struct = Option.all({ name: maybeName, age: maybeAge })
console.log(struct)
// { _id: 'Option', _tag: 'Some', value: { name: 'John', age: 25 } }
// One None makes the whole thing None
console.log(Option.all([Option.some("John"), Option.none()]))
// { _id: 'Option', _tag: 'None' }

To combine two values with a function, use Option.zipWith.

Like Effect.gen, Option.gen lets you write sequential code that short-circuits on the first None. Each yield* either unwraps a Some or aborts the whole block.

import { Option } from "effect"
const maybeName = Option.some("John")
const maybeAge = Option.some(25)
const person = Option.gen(function* () {
const name = (yield* maybeName).toUpperCase()
const age = yield* maybeAge
return { name, age }
})
console.log(person)
// { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } }

If maybeName were None, the generator would stop immediately and return Noneage would never be read. Keep these generators pure: Option is a data structure, not an effect, so avoid side effects inside them.

An Option can be used directly inside Effect.gen. A Some<A> yields its value; a None fails the effect with a NoSuchElementError, which you can then handle with the usual Error Management combinators.

import { Effect, Option } from "effect"
const program = Effect.gen(function* () {
// `yield*` on a Some produces the value;
// a None would fail with NoSuchElementError
const x = yield* Option.some(10)
const y = yield* Option.some(5)
return x + y
})
Effect.runPromise(program).then(console.log) // 15

This makes Option a convenient way to express “may be missing” steps inside an otherwise effectful workflow.