import { panic } from "./error"; import { dual } from "./dual"; /** Executes fn, panics if it throws. */ const tryOrPanic = (fn: () => T, message: string): T => { try { return fn(); } catch (cause) { throw panic(message, cause); } }; /** Async version of tryOrPanic. */ const tryOrPanicAsync = async (fn: () => Promise, message: string): Promise => { try { return await fn(); } catch (cause) { throw panic(message, cause); } }; /** * Present value variant. * * @template A Value type. * * @example * const option = new Some(42); * option.value // 42 * option.status // "some" */ export class Some { readonly status = "some" as const; constructor(readonly value: A) {} /** Returns true, narrowing Option to Some. */ isSome(): this is Some { return true; } /** Returns false, narrowing Option to None. */ isNone(): this is None { return false; } /** * Transforms value. * * @template B Transformed type. * @param fn Transformation function. * @returns Some with transformed value. * @throws {Panic} If fn throws. * * @example * some(2).map(x => x * 2) // Some(4) */ map(fn: (a: A) => B): Some { return tryOrPanic(() => new Some(fn(this.value)), "map callback threw"); } /** * Chains Option-returning function. * * @template B New value type. * @param fn Function returning Option. * @returns Option from fn. * @throws {Panic} If fn throws. * * @example * some(2).andThen(x => x > 0 ? some(x) : none()) // Some(2) */ andThen(fn: (a: A) => Option): Option { return tryOrPanic(() => fn(this.value), "andThen callback threw"); } /** * Chains async Option-returning function. * * @template B New value type. * @param fn Async function returning Option. * @returns Promise of Option from fn. * @throws {Panic} If fn throws synchronously or rejects. * * @example * await some(1).andThenAsync(async x => some(await fetchData(x))) */ andThenAsync(fn: (a: A) => Promise>): Promise> { return tryOrPanicAsync(() => fn(this.value), "andThenAsync callback threw"); } /** * Pattern matches on Option. * * @template T Return type. * @param handlers Some and none handlers. * @returns Result of some handler. * @throws {Panic} If handler throws. * * @example * some(2).match({ some: x => x * 2, none: () => 0 }) // 4 */ match(handlers: { some: (a: A) => T; none: () => T }): T { return tryOrPanic(() => handlers.some(this.value), "match some handler threw"); } /** * Extracts value. * * @param _message Ignored. * @returns The value. * * @example * some(42).unwrap() // 42 */ unwrap(_message?: string): A { return this.value; } /** * Returns value, ignoring fallback. * * @template B Fallback type. * @param _fallback Ignored. * @returns The value. * * @example * some(42).unwrapOr(0) // 42 */ unwrapOr(_fallback: B): A { return this.value; } /** * Returns value, ignoring fallback thunk. * * @template B Fallback type. * @param _fallback Ignored. * @returns The value. * * @example * some(42).unwrapOrElse(() => 0) // 42 */ unwrapOrElse(_fallback: () => B): A { return this.value; } /** * Runs side effect, returns self. * * @param fn Side effect function. * @returns Self. * @throws {Panic} If fn throws. * * @example * some(2).tap(console.log).map(x => x * 2) // logs 2, returns Some(4) */ tap(fn: (a: A) => void): Some { return tryOrPanic(() => { fn(this.value); return this; }, "tap callback threw"); } /** * Runs async side effect, returns self. * * @param fn Async side effect function. * @returns Promise of self. * @throws {Panic} If fn throws synchronously or rejects. * * @example * await some(2).tapAsync(async x => await log(x)) */ tapAsync(fn: (a: A) => Promise): Promise> { return tryOrPanicAsync(async () => { await fn(this.value); return this; }, "tapAsync callback threw"); } /** * Returns Some if predicate passes, None otherwise. * * @param predicate Test function. * @returns Some if predicate returns true, None otherwise. * @throws {Panic} If predicate throws. * * @example * some(5).filter(x => x > 3) // Some(5) * some(2).filter(x => x > 3) // None */ filter(predicate: (a: A) => boolean): Option { return tryOrPanic( () => (predicate(this.value) ? this : None.instance), "filter predicate threw", ); } /** * Makes Some yieldable in Option.gen blocks. * Immediately returns the value without yielding. */ // oxlint-disable-next-line require-yield *[Symbol.iterator](): Generator { return this.value; } } /** * Absent value variant (singleton). * * @example * const option = None.instance; * option.status // "none" */ export class None { readonly status = "none" as const; private constructor() {} /** Singleton instance. */ static readonly instance = new None(); /** Returns false, narrowing Option to Some. */ isSome(): this is Some { return false; } /** Returns true, narrowing Option to None. */ isNone(): this is None { return true; } /** * No-op on None, returns self. * * @template U New phantom value type. * @param _fn Ignored. * @returns Self. */ map(_fn: (a: never) => U): None { return this; } /** * No-op on None, returns self. * * @template U New phantom value type. * @param _fn Ignored. * @returns Self. */ andThen(_fn: (a: never) => Option): None { return this; } /** * No-op on None, returns Promise of self. * * @template U New phantom value type. * @param _fn Ignored. * @returns Promise of self. */ andThenAsync(_fn: (a: never) => Promise>): Promise { return Promise.resolve(this); } /** * Pattern matches on Option. * * @template R Return type. * @param handlers Some and none handlers. * @returns Result of none handler. * @throws {Panic} If handler throws. * * @example * none().match({ some: x => x, none: () => "empty" }) // "empty" */ match(handlers: { some: (a: never) => R; none: () => R }): R { return tryOrPanic(() => handlers.none(), "match none handler threw"); } /** * Throws error with optional message. * * @param message Error message. * @throws Always throws. * * @example * none().unwrap() // throws Panic * none().unwrap("custom") // throws Panic("custom") */ unwrap(message?: string): never { return panic(message ?? "Unwrap called on None"); } /** * Returns fallback value. * * @template U Fallback type. * @param fallback Fallback value. * @returns Fallback. * * @example * none().unwrapOr(42) // 42 */ unwrapOr(fallback: U): U { return fallback; } /** * Computes and returns fallback value. * * @template U Fallback type. * @param fallback Fallback thunk. * @returns Result of fallback. * @throws {Panic} If fallback throws. * * @example * none().unwrapOrElse(() => 42) // 42 */ unwrapOrElse(fallback: () => U): U { return tryOrPanic(fallback, "unwrapOrElse fallback threw"); } /** * No-op on None, returns self. * * @param _fn Ignored. * @returns Self. */ tap(_fn: (a: never) => void): None { return this; } /** * No-op on None, returns Promise of self. * * @param _fn Ignored. * @returns Promise of self. */ tapAsync(_fn: (a: never) => Promise): Promise { return Promise.resolve(this); } /** * No-op on None, returns self. * * @param _predicate Ignored. * @returns Self. */ filter(_predicate: (a: never) => boolean): None { return this; } /** * Makes None yieldable in Option.gen blocks. * Yields None for proper generator short-circuiting. */ *[Symbol.iterator](): Generator { yield this; return panic("Unreachable: None yielded in Option.gen but generator continued"); } } /** * Union type representing presence or absence of a value. * * @template A Value type. * * @example * type MaybeUser = Option; */ export type Option = Some | None; /** * Infer the value type from an Option. * Distributive: InferValue | Some> = A | B */ export type InferValue = O extends Some ? A : never; /** * Constraint for any Option type. * Used in Option.gen to accept flexible return types from generators. */ type AnyOption = Some | None; function some(): Some; function some(value: A): Some; function some(value?: unknown): Some { return new Some(value); } const none = (): None => None.instance; const isSome = (option: Option): option is Some => { return option.status === "some"; }; const isNone = (option: Option): option is None => { return option.status === "none"; }; const map: { (option: Option, fn: (a: A) => B): Option; (fn: (a: A) => B): (option: Option) => Option; } = dual(2, (option: Option, fn: (a: A) => B): Option => { return option.map(fn); }); const andThen: { (option: Option, fn: (a: A) => Option): Option; (fn: (a: A) => Option): (option: Option) => Option; } = dual(2, (option: Option, fn: (a: A) => Option): Option => { return option.andThen(fn); }); const andThenAsync: { (option: Option, fn: (a: A) => Promise>): Promise>; (fn: (a: A) => Promise>): (option: Option) => Promise>; } = dual(2, (option: Option, fn: (a: A) => Promise>): Promise> => { return option.andThenAsync(fn); }); const match: { (option: Option, handlers: { some: (a: A) => T; none: () => T }): T; (handlers: { some: (a: A) => T; none: () => T }): (option: Option) => T; } = dual(2, (option: Option, handlers: { some: (a: A) => T; none: () => T }): T => { return option.match(handlers); }); const tap: { (option: Option, fn: (a: A) => void): Option; (fn: (a: A) => void): (option: Option) => Option; } = dual(2, (option: Option, fn: (a: A) => void): Option => { return option.tap(fn); }); const tapAsync: { (option: Option, fn: (a: A) => Promise): Promise>; (fn: (a: A) => Promise): (option: Option) => Promise>; } = dual(2, (option: Option, fn: (a: A) => Promise): Promise> => { return option.tapAsync(fn); }); const unwrap = (option: Option, message?: string): A => { return option.unwrap(message); }; const unwrapOr: { (option: Option, fallback: B): A | B; (fallback: B): (option: Option) => A | B; } = dual(2, (option: Option, fallback: B): A | B => { return option.unwrapOr(fallback); }); /** Validates that a value is an Option instance. Throws with helpful message if not. */ function assertIsOption(value: unknown): asserts value is Option { if ( value !== null && typeof value === "object" && "status" in value && (value.status === "some" || value.status === "none") ) { return; } return panic( "Option.gen body must return Option.some() or Option.none(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)), ); } const gen: { (body: () => Generator): Option>; ( body: (this: This) => Generator, thisArg: This, ): Option>; ( body: () => AsyncGenerator, ): Promise>>; ( body: (this: This) => AsyncGenerator, thisArg: This, ): Promise>>; } = (( body: | (() => Generator) | (() => AsyncGenerator) | ((this: This) => Generator) | ((this: This) => AsyncGenerator), thisArg?: This, ): Option> | Promise>> => { // SAFETY: body.call binds thisArg; cast needed due to union of function signatures const iterator = (body as (this: This) => Generator).call(thisArg as This); // Detect async generator via Symbol.asyncIterator if (Symbol.asyncIterator in iterator) { return (async () => { // SAFETY: Async check above guarantees this is an async generator const asyncIter = iterator as unknown as AsyncGenerator; let state: IteratorResult; try { state = await asyncIter.next(); } catch (cause) { // Generator body threw before yielding (user code error or cleanup on success path) throw panic("generator body threw", cause); } assertIsOption(state.value); if (!state.done) { // Close generator to run finally blocks and Symbol.asyncDispose. // If cleanup throws, it's unrecoverable — Panic. try { await asyncIter.return?.(undefined as unknown as R); } catch (cause) { throw panic("generator cleanup threw", cause); } } return state.value as Option>; })(); } // Sync generator // SAFETY: If not async, must be sync generator const syncIter = iterator as Generator; let state: IteratorResult; try { state = syncIter.next(); } catch (cause) { // Generator body threw before yielding (user code error or cleanup on success path) throw panic("generator body threw", cause); } assertIsOption(state.value); if (!state.done) { // Close generator to run finally blocks and Symbol.dispose. // If cleanup throws, it's unrecoverable — Panic. try { syncIter.return?.(undefined as unknown as R); } catch (cause) { throw panic("generator cleanup threw", cause); } } return state.value as Option>; }) as { (body: () => Generator): Option>; ( body: (this: This) => Generator, thisArg: This, ): Option>; ( body: () => AsyncGenerator, ): Promise>>; ( body: (this: This) => AsyncGenerator, thisArg: This, ): Promise>>; }; async function* optionAwait(promise: Promise>): AsyncGenerator { const option = await promise; return yield* option; } /** Shape of a serialized Some over RPC. */ export interface SerializedSome { status: "some"; value: A; } /** Shape of a serialized None over RPC. */ export interface SerializedNone { status: "none"; } /** Shape of a serialized Option over RPC. */ export type SerializedOption = SerializedSome | SerializedNone; function isSerializedOption(obj: unknown): obj is SerializedOption { return ( obj !== null && typeof obj === "object" && "status" in obj && ((obj.status === "some" && "value" in obj) || obj.status === "none") ); } const serialize = (option: Option): SerializedOption => { return option.status === "some" ? { status: "some", value: option.value } : { status: "none" }; }; const deserialize = (value: unknown): Option | null => { if (isSerializedOption(value)) { return value.status === "some" ? (new Some(value.value) as Option) : None.instance; } return null; }; const partition = (options: readonly Option[]): [A[], number] => { const values: A[] = []; let noneCount = 0; for (const o of options) { if (o.status === "some") { values.push(o.value); } else { noneCount++; } } return [values, noneCount]; }; /** * Flattens nested Option into single Option. * * @example * const nested: Option> = Option.some(Option.some(42)); * const flat: Option = Option.flatten(nested); // Some(42) */ const flatten = (option: Option>): Option => { if (option.status === "some") { return option.value; } return option; }; /** * Converts nullable value to Option. * null and undefined become None, everything else becomes Some. */ const from = (value: A | null | undefined): Option> => { return value == null ? None.instance : new Some(value as NonNullable); }; /** * Creates Option based on predicate result. * Some if predicate passes, None otherwise. */ const fromPredicate: { (value: A, predicate: (a: A) => a is B): Option; (value: A, predicate: (a: A) => boolean): Option; (predicate: (a: A) => a is B): (value: A) => Option; (predicate: (a: A) => boolean): (value: A) => Option; } = dual(2, (value: A, predicate: (a: A) => boolean): Option => { return tryOrPanic( () => (predicate(value) ? new Some(value) : None.instance), "fromPredicate predicate threw", ); }); /** * Filters Option based on predicate. * Some becomes None if predicate fails. */ const filter: { (option: Option, predicate: (a: A) => a is B): Option; (option: Option, predicate: (a: A) => boolean): Option; (predicate: (a: A) => a is B): (option: Option) => Option; (predicate: (a: A) => boolean): (option: Option) => Option; } = dual(2, (option: Option, predicate: (a: A) => boolean): Option => { return option.filter(predicate); }); /** * Combines two Options into Option of tuple. * Returns Some([a, b]) if both are Some, None otherwise. */ const zip = (optionA: Option, optionB: Option): Option<[A, B]> => { if (optionA.status === "some" && optionB.status === "some") { return new Some<[A, B]>([optionA.value, optionB.value]); } return None.instance; }; /** * Extracts value or computes fallback lazily. */ const unwrapOrElse: { (option: Option, fallback: () => B): A | B; (fallback: () => B): (option: Option) => A | B; } = dual(2, (option: Option, fallback: () => B): A | B => { return option.unwrapOrElse(fallback); }); /** * Utilities for creating and handling Option types. * * @example * const option = Option.from(maybeNull); * const value = option.map(x => x.id).unwrapOr("default"); */ export const Option = { /** * Creates present value. * * @example * Option.some(42) // Some * Option.some() // Some */ some, /** * Creates absent value. * * @example * Option.none() // None */ none, /** * Type guard for Some. * * @example * if (Option.isSome(option)) { option.value } */ isSome, /** * Type guard for None. * * @example * if (Option.isNone(option)) { // handle absent } */ isNone, /** * Transforms value, passes None through. * * @example * Option.map(some(2), x => x * 2) // Some(4) * Option.map(x => x * 2)(some(2)) // Some(4) */ map, /** * Chains Option-returning function on Some. * * @example * Option.andThen(some(2), x => x > 0 ? some(x) : none()) // Some(2) */ andThen, /** * Chains async Option-returning function on Some. * * @example * await Option.andThenAsync(some(1), async x => some(await fetch(x))) */ andThenAsync, /** * Pattern matches on Option. * * @example * Option.match(some(2), { some: x => x * 2, none: () => 0 }) // 4 */ match, /** * Runs side effect on Some value, returns original option. * * @example * Option.tap(some(2), console.log) // logs 2, returns Some(2) */ tap, /** * Runs async side effect on Some value, returns original option. * * @example * await Option.tapAsync(some(2), async x => await log(x)) */ tapAsync, /** * Extracts value or throws. * * @example * Option.unwrap(some(42)) // 42 * Option.unwrap(none()) // throws Panic */ unwrap, /** * Extracts value or returns fallback. * * @example * Option.unwrapOr(some(42), 0) // 42 * Option.unwrapOr(none(), 0) // 0 */ unwrapOr, /** * Generator-based composition for Option types. * * @example * const option = Option.gen(function* () { * const a = yield* getA(); // Option * const b = yield* getB(a); // Option * return Option.some({ a, b }); * }); * // Option<{a, b}> * * @example * // Async with Option.await * const option = await Option.gen(async function* () { * const a = yield* Option.await(fetchA()); * const b = yield* Option.await(fetchB(a)); * return Option.some({ a, b }); * }); */ gen, /** * Wraps Promise