Maybe
Maybe<T> represents a value that may or may not exist. Instead of scattering null checks across your code, you wrap optional values in a Maybe and use typed combinators to transform, filter, and extract them safely.
type Maybe<T> = { kind: "Just"; value: T } | { kind: "Nothing" };import * as M from "@oofp/core/maybe";Constructors
Section titled “Constructors”Wraps a value in a Just.
M.just(42); // { kind: "Just", value: 42 }M.just("hello"); // { kind: "Just", value: "hello" }nothing
Section titled “nothing”Creates a Nothing — a Maybe with no value.
M.nothing(); // { kind: "Nothing" }fromNullable
Section titled “fromNullable”Converts a nullable value (T | null | undefined) into a Maybe<T>. null and undefined become Nothing; everything else becomes Just.
M.fromNullable(42); // Just(42)M.fromNullable(null); // NothingM.fromNullable(undefined); // NothingM.fromNullable(""); // Just("") — empty string is still a valueM.fromNullable(0); // Just(0) — zero is still a valueThis is your primary bridge from nullable APIs (DOM, JSON, database results) into the Maybe world:
import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
const element = pipe( M.fromNullable(document.getElementById("app")), M.map((el) => el.textContent), M.chain(M.fromNullable), M.getOrElse(""),);Alias for just. Lifts a value into Maybe.
M.of(10); // Just(10)Mapping
Section titled “Mapping”Transforms the value inside a Just. Does nothing to Nothing.
pipe(M.just(5), M.map((n) => n * 2)); // Just(10)pipe(M.nothing(), M.map((n) => n * 2)); // NothingLike map, but the transformation function itself returns a Maybe. Flattens the result so you don’t end up with Maybe<Maybe<T>>.
const safeDivide = (a: number, b: number): M.Maybe<number> => b === 0 ? M.nothing() : M.just(a / b);
pipe(M.just(10), M.chain((n) => safeDivide(n, 2))); // Just(5)pipe(M.just(10), M.chain((n) => safeDivide(n, 0))); // Nothingpipe(M.nothing(), M.chain((n) => safeDivide(n, 2))); // NothingFlattens a nested Maybe<Maybe<T>> into Maybe<T>.
pipe(M.just(M.just(42)), M.join); // Just(42)pipe(M.just(M.nothing()), M.join); // Nothingpipe(M.nothing(), M.join); // NothingchainNothing
Section titled “chainNothing”Provides a fallback Maybe when the current value is Nothing. The inverse of chain — it runs only on Nothing and passes Just values through unchanged.
const fromCache = (): M.Maybe<string> => M.nothing();const fromApi = (): M.Maybe<string> => M.just("fetched");
pipe(fromCache(), M.chainNothing(fromApi)); // Just("fetched")pipe(M.just("cached"), M.chainNothing(fromApi)); // Just("cached")Guards
Section titled “Guards”isJust
Section titled “isJust”Type guard that narrows a Maybe<T> to Just<T>.
const m = M.fromNullable(42);
if (M.isJust(m)) { console.log(m.value); // TypeScript knows m.value exists}isNothing
Section titled “isNothing”Type guard that narrows a Maybe<T> to Nothing.
const m = M.fromNullable(null);
if (M.isNothing(m)) { console.log("No value present");}Filtering
Section titled “Filtering”Keeps the value if the predicate returns true; otherwise returns Nothing. Stands for “if and only if”.
pipe(M.just(18), M.iif((age) => age >= 18)); // Just(18)pipe(M.just(15), M.iif((age) => age >= 18)); // Nothingpipe(M.nothing(), M.iif((age) => age >= 18)); // NothingUseful for validation pipelines:
import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
const validateEmail = (input: string) => pipe( M.fromNullable(input), M.iif((s) => s.includes("@")), M.iif((s) => s.length >= 5), );
validateEmail("a@b.co"); // Just("a@b.co")validateEmail("nope"); // NothingiifNot
Section titled “iifNot”The inverse of iif. Keeps the value if the predicate returns false.
pipe(M.just("active"), M.iifNot((s) => s === "banned")); // Just("active")pipe(M.just("banned"), M.iifNot((s) => s === "banned")); // NothingSide Effects
Section titled “Side Effects”Executes a side effect for Just values without altering the Maybe. Returns the original Maybe unchanged.
pipe( M.just("data"), M.tap((value) => console.log("Got:", value)), M.map((s) => s.toUpperCase()),);// Logs: "Got: data"// Result: Just("DATA")tapNothing
Section titled “tapNothing”Executes a side effect when the value is Nothing. Returns the original Maybe unchanged.
pipe( M.nothing(), M.tapNothing(() => console.warn("Value was missing")), M.getOrElse("default"),);// Logs: "Value was missing"// Result: "default"Extraction
Section titled “Extraction”These functions unwrap a Maybe into a plain value. Use them at the boundaries of your program — after you’ve done all your transformations.
Pattern-matches on a Maybe, providing handlers for both cases.
pipe( M.just(42), M.fold( () => "nothing here", (value) => `got ${value}`, ),);// "got 42"
pipe( M.nothing(), M.fold( () => "nothing here", (value) => `got ${value}`, ),);// "nothing here"getOrElse
Section titled “getOrElse”Returns the value if Just, otherwise returns the provided default.
pipe(M.just(42), M.getOrElse(0)); // 42pipe(M.nothing(), M.getOrElse(0)); // 0toNullable
Section titled “toNullable”Converts a Maybe<T> to T | null.
M.toNullable(M.just(42)); // 42M.toNullable(M.nothing()); // nulltoUndefined
Section titled “toUndefined”Converts a Maybe<T> to T | undefined.
M.toUndefined(M.just(42)); // 42M.toUndefined(M.nothing()); // undefinedApplicative
Section titled “Applicative”Applies a function inside a Maybe to a value inside another Maybe. Both must be Just for the result to be Just.
const add = (a: number) => (b: number) => a + b;
pipe(M.just(add), M.apply(M.just(3)), M.apply(M.just(4)));// Just(7)
pipe(M.just(add), M.apply(M.nothing()), M.apply(M.just(4)));// NothingliftA2
Section titled “liftA2”Lifts a binary function to work with two Maybe values. If both are Just, applies the function; if either is Nothing, returns Nothing.
const add = (a: number, b: number) => a + b;
M.liftA2(add)(M.just(3), M.just(4)); // Just(7)M.liftA2(add)(M.just(3), M.nothing()); // NothingM.liftA2(add)(M.nothing(), M.just(4)); // NothingThis is particularly useful when you need to combine exactly two optional values:
import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
interface Config { host: string; port: number;}
const host = M.fromNullable(process.env.HOST);const port = pipe( M.fromNullable(process.env.PORT), M.map(Number), M.iif((n) => !isNaN(n)),);
const config = M.liftA2((h: string, p: number): Config => ({ host: h, port: p }))( host, port,);// Just({ host: "localhost", port: 3000 }) or NothingCombining
Section titled “Combining”sequence
Section titled “sequence”Transforms an array of Maybe values into a Maybe of an array. If all are Just, returns Just with all values. If any is Nothing, returns Nothing.
M.sequence([M.just(1), M.just(2), M.just(3)]);// Just([1, 2, 3])
M.sequence([M.just(1), M.nothing(), M.just(3)]);// NothingsequenceObject
Section titled “sequenceObject”Like sequence, but for objects. Takes an object where each value is a Maybe, and returns a Maybe of the unwrapped object. If all values are Just, returns Just with the full object. If any is Nothing, returns Nothing.
M.sequenceObject({ name: M.just("Alice"), age: M.just(30), email: M.just("alice@example.com"),});// Just({ name: "Alice", age: 30, email: "alice@example.com" })
M.sequenceObject({ name: M.just("Alice"), age: M.nothing(), email: M.just("alice@example.com"),});// NothingThis is ideal for building validated objects from multiple optional sources:
import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
interface UserForm { name: string; email: string; age: number;}
const parseForm = (data: Record<string, string | undefined>): M.Maybe<UserForm> => M.sequenceObject({ name: pipe( M.fromNullable(data.name), M.iif((s) => s.length > 0), ), email: pipe( M.fromNullable(data.email), M.iif((s) => s.includes("@")), ), age: pipe( M.fromNullable(data.age), M.map(Number), M.iif((n) => !isNaN(n) && n > 0), ), });
parseForm({ name: "Alice", email: "alice@example.com", age: "30" });// Just({ name: "Alice", email: "alice@example.com", age: 30 })
parseForm({ name: "Alice", email: "bad", age: "30" });// NothingPractical Examples
Section titled “Practical Examples”Working with DOM elements
Section titled “Working with DOM elements”import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
const getInputValue = (id: string): M.Maybe<string> => pipe( M.fromNullable(document.getElementById(id)), M.map((el) => (el as HTMLInputElement).value), M.iif((v) => v.trim().length > 0), );
const greeting = pipe( getInputValue("name-field"), M.map((name) => `Hello, ${name}!`), M.getOrElse("Hello, stranger!"),);Safe property access
Section titled “Safe property access”import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
interface User { name: string; address?: { city?: string; };}
const getCity = (user: User): M.Maybe<string> => pipe( M.fromNullable(user.address), M.chain((addr) => M.fromNullable(addr.city)), );
getCity({ name: "Alice", address: { city: "NYC" } }); // Just("NYC")getCity({ name: "Bob", address: {} }); // NothinggetCity({ name: "Charlie" }); // NothingFallback chains with chainNothing
Section titled “Fallback chains with chainNothing”import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
const getSetting = (key: string): M.Maybe<string> => pipe( M.fromNullable(localStorage.getItem(key)), M.chainNothing(() => M.fromNullable(sessionStorage.getItem(key))), M.chainNothing(() => M.fromNullable(getDefaultSetting(key))), );Combining multiple optional fields
Section titled “Combining multiple optional fields”import { pipe } from "@oofp/core/pipe";import * as M from "@oofp/core/maybe";
interface ApiResponse { user?: { id?: string; name?: string }; permissions?: string[];}
const extractUserInfo = (response: ApiResponse) => pipe( M.sequenceObject({ id: pipe( M.fromNullable(response.user), M.chain((u) => M.fromNullable(u.id)), ), name: pipe( M.fromNullable(response.user), M.chain((u) => M.fromNullable(u.name)), ), permissions: M.fromNullable(response.permissions), }), M.iif((info) => info.permissions.length > 0), M.tap((info) => console.log(`Loaded user ${info.name}`)), );