Skip to content

Either

The Either type represents values that can be one of two types: a Left containing an error value, or a Right containing a success value.

It is commonly used for error handling and expressing computations that may fail. Instead of throwing exceptions, functions can return an Either that explicitly represents both success and failure cases.

Throwing exceptions complicates control flow and hides exceptions from the type system, making it harder to reason about error handling in your code.

Functions that can fail with exceptions can become hard to compose and manage. Exception handling adds complexity and makes control flow harder to follow.

import { expect } from "jsr:@std/expect"
const divide = (a: number, b: number): number => {
if (b === 0) {
throw new Error("Division by zero")
}
return a / b
}
const increment = (a: number): number => a + 1
try {
expect(increment(divide(6, 2))).toEqual(4)
expect(increment(divide(6, 0))) // Throws error
} catch (e) {
// handle error
}

Using Either makes error handling explicit and composable. The error cases are handled through the type system rather than exceptions.

import { expect } from "jsr:@std/expect"
import { Either, pipe } from "@jvlk/fp-tsm"
const divide = (a: number, b: number): Either.Either<string, number> =>
b === 0 ? Either.left("Division by zero") : Either.right(a / b)
const increment = (a: Either.Either<string, number>): Either.Either<string, number> =>
pipe(
a,
Either.map(x => x + 1)
)
expect(increment(divide(6, 2))).toEqual(Either.right(4))
expect(increment(divide(6, 0))).toEqual(Either.left("Division by zero"))

Constructs a Right. Represents a successful value in an Either.

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
// when creating an Either using `left` or `right` you should provide the type parameters
expect(Either.right(1)).toEqual({ _tag: "Right", right: 1 })
expect(Either.right("hello")).toEqual({ _tag: "Right", right: "hello" })

Constructs a Left. Represents a failure value in an Either.

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
// when creating an Either using `left` or `right` you should provide the type parameters
expect(Either.left("error")).toEqual({ _tag: "Left", left: "error" })
expect(Either.left(404)).toEqual({ _tag: "Left", left: 404 })

tryCatch is a utility function that allows you to execute a function that may throw an unknown error and return an Either.

It can take an optional second argument, catchFn, which is a function that transforms the unknown error into something else.

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
expect(Either.tryCatch(() => 1)).toEqual(Either.right(1))
expect(Either.tryCatch(() => { throw new Error("Error") }))
.toEqual(Either.left(Error("Error")))
expect(Either.tryCatch(() => { throw new Error("something went wrong") }, (e) => `Caught: ${e}`)).toEqual(Either.left("Caught: Error: something went wrong"))

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

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
const isPositive = Either.fromPredicate(
(n: number): n is number => n >= 0,
() => "Number must be positive"
)
expect(isPositive(-1)).toEqual(Either.left("Number must be positive"))
expect(isPositive(1)).toEqual(Either.right(1))

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

Mapping a Value in Right

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

Mapping over Left

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
// Mapping over Left results in the same Left
expect(Either.map(Either.left("error"), (n: number) => n + 1)).toEqual(Either.left("error"))

The Either.mapLeft function lets you transform the error value inside an Either without manually unwrapping and re-wrapping it. If the Either holds a left value (Left), the transformation function is applied. If the Either is Right, the function is ignored, and the Right value remains unchanged.

Mapping an Error Value in Left

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
// Transform the error value inside Left
expect(Either.mapLeft(Either.left("error"), (s: string) => s.toUpperCase()))
.toEqual(Either.left("ERROR"))

Mapping Left over Right

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
// Mapping Left over Right results in the same Right
expect(Either.mapLeft(Either.right(1), (s: string) => s.toUpperCase()))
.toEqual(Either.right(1))

Maps over both parts of an Either simultaneously using two functions. If the Either is Left, applies the first function; if Right, applies the second function.

import { expect } from "jsr:@std/expect"
import { Either, pipe } from "@jvlk/fp-tsm"
expect(Either.bimap(Either.right(1),
(e: string) => e.toUpperCase(),
(n: number) => n + 1
)).toEqual(Either.right(2))
expect(Either.bimap(Either.left("error"),
(e: string) => e.toUpperCase(),
(n: number) => n + 1
)).toEqual(Either.left("ERROR"))
expect(
pipe(
Either.right(1),
Either.bimap(
(e: string) => e.toUpperCase(),
(n: number) => n + 1
)
)
).toEqual(Either.right(2))

Applies a function to the value of a Right and flattens the resulting Either. If the input is Left, it remains Left.

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

This utility is particularly useful for sequencing operations that may fail, enabling clean and concise workflows for handling error cases.

import { expect } from "jsr:@std/expect"
import { Either, pipe } from "@jvlk/fp-tsm"
type Error = string
type User = {
readonly id: number
readonly address: Either.Either<Error, string>
}
const user: User = {
id: 1,
address: Either.right("123 Main St")
}
const validateAddress = (address: string): Either.Either<Error, string> =>
address.length > 0 ? Either.right(address) : Either.left("Invalid address")
const result = pipe(
user.address,
Either.flatMap(validateAddress)
)
expect(result).toEqual(Either.right("123 Main St"))

Applies a function to the value of a Left and flattens the resulting Either. If the input is Right, it remains Right.

This function is the left-sided equivalent of flatMap. It allows you to chain computations on the Left value while preserving any Right value unchanged.

import { Either, pipe } from "@jvlk/fp-tsm"
const result = pipe(
Either.left("error"),
Either.flatMapLeft(error => Either.left(error.toUpperCase()))
)
// Result: Either.left("ERROR")

Matches an Either against two functions: one for the Left case and one for the Right 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 { Either, pipe } from "@jvlk/fp-tsm"
expect(Either.match(Either.right(42), e => `Error: ${e}`, val => `The value is ${val}.`)).toEqual("The value is 42.")
expect(Either.match(Either.left("error"), e => `Error: ${e}`, val => `The value is ${val}.`)).toEqual("Error: error")
expect(
pipe(
Either.right(42),
Either.match(
e => `Error: ${e}`,
val => `The value is ${val}.`
)
)
).toEqual("The value is 42.")
expect(
pipe(
Either.left("error"),
Either.match(
e => `Error: ${e}`,
val => `The value is ${val}.`
)
)
).toEqual("Error: error")

Returns the value inside a Right, or the result of onLeft if the Either is a Left.

import { expect } from "jsr:@std/expect"
import { Either, pipe } from "@jvlk/fp-tsm"
expect(Either.getOrElse(Either.right(1), () => 0)).toEqual(1)
expect(Either.getOrElse(Either.left("error"), () => 0)).toEqual(0)
expect(
pipe(
Either.right(42),
Either.getOrElse(() => 10),
)
).toEqual(42)
expect(
pipe(
Either.left("error"),
Either.getOrElse(() => 10),
)
).toEqual(10)

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

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
expect(Either.isRight(Either.right(1))).toEqual(true)
expect(Either.isRight(Either.left("error"))).toEqual(false)

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

import { expect } from "jsr:@std/expect"
import { Either } from "@jvlk/fp-tsm"
expect(Either.isLeft(Either.right(1))).toEqual(false)
expect(Either.isLeft(Either.left("error"))).toEqual(true)

Converts an Option to an Either. If the Option is Some, it returns Right with the contained value; if it is None, it returns Left with the provided error value.

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

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

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

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

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

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