Last active
November 27, 2025 21:52
-
-
Save dmmulroy/720dcad37049ad8f1d0e75c06e133b1e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { describe, expect, it } from "vitest"; | |
| import { Result } from "./result"; | |
| describe("Ok", () => { | |
| it("isOk returns true", () => { | |
| expect(Result.ok(1).isOk()).toBe(true); | |
| }); | |
| it("isErr returns false", () => { | |
| expect(Result.ok(1).isErr()).toBe(false); | |
| }); | |
| it("map transforms value", () => { | |
| const result = Result.ok(2).map((x) => x * 3); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("mapErr returns same instance", () => { | |
| const original = Result.ok<number, string>(1); | |
| const mapped = original.mapErr((e) => e.length); | |
| expect(mapped).toBe(original); | |
| }); | |
| it("andThen chains operations", () => { | |
| const result = Result.ok(2).andThen((x) => Result.ok(x * 3)); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("andThen propagates errors", () => { | |
| const result = Result.ok(2).andThen(() => Result.err("fail")); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| it("orElse returns same instance", () => { | |
| const original = Result.ok<number, string>(1); | |
| const result = original.orElse(() => Result.ok(2)); | |
| expect(result).toBe(original); | |
| }); | |
| it("unwrap returns value", () => { | |
| expect(Result.ok(42).unwrap()).toBe(42); | |
| }); | |
| it("unwrapOr returns value", () => { | |
| expect(Result.ok(42).unwrapOr(0)).toBe(42); | |
| }); | |
| it("unwrapErr throws", () => { | |
| expect(() => Result.ok(1).unwrapErr()).toThrow("Called unwrapErr on Ok"); | |
| }); | |
| it("match calls ok handler", () => { | |
| const result = Result.ok(5).match({ | |
| ok: (v) => v * 2, | |
| err: () => 0, | |
| }); | |
| expect(result).toBe(10); | |
| }); | |
| }); | |
| describe("Err", () => { | |
| it("isOk returns false", () => { | |
| expect(Result.err("fail").isOk()).toBe(false); | |
| }); | |
| it("isErr returns true", () => { | |
| expect(Result.err("fail").isErr()).toBe(true); | |
| }); | |
| it("map returns same instance", () => { | |
| const original = Result.err<string, number>("fail"); | |
| const mapped = original.map((x) => x * 2); | |
| expect(mapped).toBe(original); | |
| }); | |
| it("mapErr transforms error", () => { | |
| const result = Result.err("fail").mapErr((e) => e.length); | |
| expect(result.isErr() && result.error).toBe(4); | |
| }); | |
| it("andThen returns same instance", () => { | |
| const original = Result.err<string, number>("fail"); | |
| const result = original.andThen((x) => Result.ok(x * 2)); | |
| expect(result).toBe(original); | |
| }); | |
| it("orElse calls recovery function", () => { | |
| const result = Result.err<string, number>("fail").orElse(() => | |
| Result.ok<number, string>(42), | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("unwrap throws", () => { | |
| expect(() => Result.err("fail").unwrap()).toThrow("Called unwrap on Err"); | |
| }); | |
| it("unwrapOr returns default", () => { | |
| expect(Result.err<string, number>("fail").unwrapOr(0)).toBe(0); | |
| }); | |
| it("unwrapErr returns error", () => { | |
| expect(Result.err("fail").unwrapErr()).toBe("fail"); | |
| }); | |
| it("match calls err handler", () => { | |
| const result = Result.err<string, number>("fail").match({ | |
| ok: () => 0, | |
| err: (e) => e.length, | |
| }); | |
| expect(result).toBe(4); | |
| }); | |
| }); | |
| describe("tryCatch", () => { | |
| it("returns Ok on success", () => { | |
| const result = Result.tryCatch(() => 42); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("returns Err on throw", () => { | |
| const result = Result.tryCatch( | |
| () => { | |
| throw new Error("oops"); | |
| }, | |
| (e) => (e as Error).message, | |
| ); | |
| expect(result.isErr() && result.error).toBe("oops"); | |
| }); | |
| it("uses default error handler", () => { | |
| const error = new Error("oops"); | |
| const result = Result.tryCatch(() => { | |
| throw error; | |
| }); | |
| expect(result.isErr() && result.error).toBe(error); | |
| }); | |
| }); | |
| describe("tryCatchAsync", () => { | |
| it("returns Ok on success", async () => { | |
| const result = await Result.tryCatchAsync(async () => 42); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("returns Err on rejection", async () => { | |
| const result = await Result.tryCatchAsync( | |
| async () => { | |
| throw new Error("oops"); | |
| }, | |
| (e) => (e as Error).message, | |
| ); | |
| expect(result.isErr() && result.error).toBe("oops"); | |
| }); | |
| }); | |
| describe("all", () => { | |
| it("returns Ok with all values on success", () => { | |
| const result = Result.all([Result.ok(1), Result.ok(2), Result.ok(3)]); | |
| expect(result.isOk() && result.value).toEqual([1, 2, 3]); | |
| }); | |
| it("returns first Err on failure", () => { | |
| const result = Result.all([ | |
| Result.ok(1), | |
| Result.err("first"), | |
| Result.ok(3), | |
| Result.err("second"), | |
| ]); | |
| expect(result.isErr() && result.error).toBe("first"); | |
| }); | |
| it("returns same Err instance (no allocation)", () => { | |
| const e = Result.err("fail"); | |
| const result = Result.all([Result.ok(1), e, Result.ok(3)]); | |
| expect(result).toBe(e); | |
| }); | |
| it("returns Ok for empty array", () => { | |
| const result = Result.all([]); | |
| expect(result.isOk() && result.value).toEqual([]); | |
| }); | |
| }); | |
| describe("partition", () => { | |
| it("returns Ok with all values on success", () => { | |
| const result = Result.partition([Result.ok(1), Result.ok(2), Result.ok(3)]); | |
| expect(result.isOk() && result.value).toEqual([1, 2, 3]); | |
| }); | |
| it("returns Err with all errors on failure", () => { | |
| const result = Result.partition([ | |
| Result.ok(1), | |
| Result.err("a"), | |
| Result.ok(2), | |
| Result.err("b"), | |
| ]); | |
| expect(result.isErr() && result.error).toEqual(["a", "b"]); | |
| }); | |
| it("returns Ok for empty array", () => { | |
| const result = Result.partition([]); | |
| expect(result.isOk() && result.value).toEqual([]); | |
| }); | |
| }); | |
| describe("firstOk", () => { | |
| it("returns first Ok", () => { | |
| const result = Result.firstOk([ | |
| Result.err("a"), | |
| Result.err("b"), | |
| Result.ok(1), | |
| Result.ok(2), | |
| ]); | |
| expect(result.isOk() && result.value).toBe(1); | |
| }); | |
| it("returns last Err when all fail", () => { | |
| const result = Result.firstOk([ | |
| Result.err("a"), | |
| Result.err("b"), | |
| Result.err("c"), | |
| ]); | |
| expect(result.isErr() && result.error).toBe("c"); | |
| }); | |
| it("throws on empty array", () => { | |
| expect(() => Result.firstOk([])).toThrow("firstOk called with empty array"); | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Result type for typed error handling without exceptions. | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.ok(42) | |
| * .map(x => x * 2) | |
| * .andThen(x => x > 50 ? Result.ok(x) : Result.err("too small")); | |
| * | |
| * if (result.isOk()) { | |
| * console.log(result.value); | |
| * } else { | |
| * console.log(result.error); | |
| * } | |
| * ``` | |
| */ | |
| export type Result<T, E> = Ok<T, E> | Err<T, E>; | |
| // SAFETY: Ok only stores `value: T`. The `E` type parameter is phantom (unused at runtime). | |
| // Casting Ok<T, E> to Ok<T, F> is safe because E has no runtime representation. | |
| /** | |
| * Success variant of Result. | |
| * | |
| * @template T - The success value type | |
| * @template E - The error type (phantom, unused at runtime) | |
| */ | |
| export class Ok<T, E> { | |
| readonly _tag = "Ok" as const; | |
| constructor(readonly value: T) {} | |
| /** | |
| * Type guard for Ok variant. | |
| * | |
| * @returns true | |
| */ | |
| isOk(): this is Ok<T, E> { | |
| return true; | |
| } | |
| /** | |
| * Type guard for Err variant. | |
| * | |
| * @returns false | |
| */ | |
| isErr(): this is Err<T, E> { | |
| return false; | |
| } | |
| /** | |
| * Transform the success value. | |
| * | |
| * @param fn - Transform function | |
| * @returns New Result with transformed value | |
| */ | |
| map<U>(fn: (value: T) => U): Result<U, E> { | |
| return new Ok(fn(this.value)); | |
| } | |
| /** | |
| * Transform the error value. No-op for Ok. | |
| * | |
| * @param _fn - Transform function (not called) | |
| * @returns This Ok with new error type | |
| */ | |
| mapErr<F>(_fn: (error: E) => F): Result<T, F> { | |
| // SAFETY: E is phantom in Ok; see class comment | |
| return this as unknown as Ok<T, F>; | |
| } | |
| /** | |
| * Chain a Result-returning function on success. | |
| * | |
| * @param fn - Function returning a new Result | |
| * @returns Result from fn | |
| */ | |
| andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F> { | |
| return fn(this.value); | |
| } | |
| /** | |
| * Recover from error with a Result-returning function. No-op for Ok. | |
| * | |
| * @param _fn - Recovery function (not called) | |
| * @returns This Ok with new error type | |
| */ | |
| orElse<F>(_fn: (error: E) => Result<T, F>): Result<T, F> { | |
| // SAFETY: E is phantom in Ok; see class comment | |
| return this as unknown as Ok<T, F>; | |
| } | |
| /** | |
| * Extract success value. | |
| * | |
| * @returns The success value | |
| */ | |
| unwrap(): T { | |
| return this.value; | |
| } | |
| /** | |
| * Extract success value or return default. | |
| * | |
| * @param _defaultValue - Default value (not used) | |
| * @returns The success value | |
| */ | |
| unwrapOr(_defaultValue: T): T { | |
| return this.value; | |
| } | |
| /** | |
| * Extract error value. | |
| * | |
| * @throws Error always (Ok has no error) | |
| */ | |
| unwrapErr(): E { | |
| throw new Error("Called unwrapErr on Ok"); | |
| } | |
| /** | |
| * Pattern match on Result. | |
| * | |
| * @param handlers - Object with ok and err handlers | |
| * @returns Result of ok handler | |
| */ | |
| match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U { | |
| return handlers.ok(this.value); | |
| } | |
| } | |
| // SAFETY: Err only stores `error: E`. The `T` type parameter is phantom (unused at runtime). | |
| // Casting Err<T, E> to Err<U, E> is safe because T has no runtime representation. | |
| /** | |
| * Failure variant of Result. | |
| * | |
| * @template T - The success type (phantom, unused at runtime) | |
| * @template E - The error value type | |
| */ | |
| export class Err<T, E> { | |
| readonly _tag = "Err" as const; | |
| constructor(readonly error: E) {} | |
| /** | |
| * Type guard for Ok variant. | |
| * | |
| * @returns false | |
| */ | |
| isOk(): this is Ok<T, E> { | |
| return false; | |
| } | |
| /** | |
| * Type guard for Err variant. | |
| * | |
| * @returns true | |
| */ | |
| isErr(): this is Err<T, E> { | |
| return true; | |
| } | |
| /** | |
| * Transform the success value. No-op for Err. | |
| * | |
| * @param _fn - Transform function (not called) | |
| * @returns This Err with new success type | |
| */ | |
| map<U>(_fn: (value: T) => U): Result<U, E> { | |
| // SAFETY: T is phantom in Err; see class comment | |
| return this as unknown as Err<U, E>; | |
| } | |
| /** | |
| * Transform the error value. | |
| * | |
| * @param fn - Transform function | |
| * @returns New Err with transformed error | |
| */ | |
| mapErr<F>(fn: (error: E) => F): Result<T, F> { | |
| return new Err(fn(this.error)); | |
| } | |
| /** | |
| * Chain a Result-returning function on success. No-op for Err. | |
| * | |
| * @param _fn - Function returning a new Result (not called) | |
| * @returns This Err with union error type | |
| */ | |
| andThen<U, F>(_fn: (value: T) => Result<U, F>): Result<U, E | F> { | |
| // SAFETY: T is phantom in Err; see class comment | |
| return this as unknown as Err<U, E>; | |
| } | |
| /** | |
| * Recover from error with a Result-returning function. | |
| * | |
| * @param fn - Recovery function | |
| * @returns Result from fn | |
| */ | |
| orElse<F>(fn: (error: E) => Result<T, F>): Result<T, F> { | |
| return fn(this.error); | |
| } | |
| /** | |
| * Extract success value. | |
| * | |
| * @throws Error always (Err has no success value) | |
| */ | |
| unwrap(): T { | |
| throw new Error("Called unwrap on Err"); | |
| } | |
| /** | |
| * Extract success value or return default. | |
| * | |
| * @param defaultValue - Default value to return | |
| * @returns The default value | |
| */ | |
| unwrapOr(defaultValue: T): T { | |
| return defaultValue; | |
| } | |
| /** | |
| * Extract error value. | |
| * | |
| * @returns The error value | |
| */ | |
| unwrapErr(): E { | |
| return this.error; | |
| } | |
| /** | |
| * Pattern match on Result. | |
| * | |
| * @param handlers - Object with ok and err handlers | |
| * @returns Result of err handler | |
| */ | |
| match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U { | |
| return handlers.err(this.error); | |
| } | |
| } | |
| /** | |
| * Create a success Result. | |
| * | |
| * @param value - The success value | |
| * @returns Ok containing value | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.ok(42); | |
| * ``` | |
| */ | |
| function ok<T, E = never>(value: T): Result<T, E> { | |
| return new Ok(value); | |
| } | |
| /** | |
| * Create a failure Result. | |
| * | |
| * @param error - The error value | |
| * @returns Err containing error | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.err("not found"); | |
| * ``` | |
| */ | |
| function err<E, T = never>(error: E): Result<T, E> { | |
| return new Err(error); | |
| } | |
| /** | |
| * Wrap a throwing function in a Result. | |
| * | |
| * @param fn - Function that may throw | |
| * @param onError - Transform caught value to error type | |
| * @returns Ok with return value or Err with transformed error | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.tryCatch( | |
| * () => JSON.parse(input), | |
| * (e) => new ParseError({ cause: e }) | |
| * ); | |
| * ``` | |
| */ | |
| function tryCatch<T, E = Error>( | |
| fn: () => T, | |
| onError: (e: unknown) => E = (e) => e as E, | |
| ): Result<T, E> { | |
| try { | |
| return ok(fn()); | |
| } catch (e) { | |
| return err(onError(e)); | |
| } | |
| } | |
| /** | |
| * Wrap an async throwing function in a Result. | |
| * | |
| * @param fn - Async function that may throw | |
| * @param onError - Transform caught value to error type | |
| * @returns Promise of Ok with return value or Err with transformed error | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = await Result.tryCatchAsync( | |
| * () => fetch(url).then(r => r.json()), | |
| * (e) => new FetchError({ cause: e }) | |
| * ); | |
| * ``` | |
| */ | |
| async function tryCatchAsync<T, E = Error>( | |
| fn: () => Promise<T>, | |
| onError: (e: unknown) => E = (e) => e as E, | |
| ): Promise<Result<T, E>> { | |
| try { | |
| return ok(await fn()); | |
| } catch (e) { | |
| return err(onError(e)); | |
| } | |
| } | |
| /** | |
| * Convert array of Results to Result of array. Fails on first error. | |
| * | |
| * @param results - Array of Results | |
| * @returns Ok with array of values or first Err encountered | |
| * | |
| * @example | |
| * ```typescript | |
| * const results = [Result.ok(1), Result.ok(2), Result.ok(3)]; | |
| * const combined = Result.all(results); // Ok([1, 2, 3]) | |
| * ``` | |
| */ | |
| function all<T, E>(results: Result<T, E>[]): Result<T[], E> { | |
| const values: T[] = []; | |
| for (const result of results) { | |
| if (result.isErr()) { | |
| // SAFETY: T is phantom in Err; Err<T, E> and Err<T[], E> are identical at runtime | |
| return result as unknown as Err<T[], E>; | |
| } | |
| values.push(result.value); | |
| } | |
| return ok(values); | |
| } | |
| /** | |
| * Convert array of Results to Result of array. Collects all errors. | |
| * | |
| * @param results - Array of Results | |
| * @returns Ok with array of values or Err with array of all errors | |
| * | |
| * @example | |
| * ```typescript | |
| * const results = [Result.ok(1), Result.err("a"), Result.err("b")]; | |
| * const combined = Result.partition(results); // Err(["a", "b"]) | |
| * ``` | |
| */ | |
| function partition<T, E>(results: Result<T, E>[]): Result<T[], E[]> { | |
| const values: T[] = []; | |
| const errors: E[] = []; | |
| for (const result of results) { | |
| if (result.isErr()) { | |
| errors.push(result.error); | |
| } else { | |
| values.push(result.value); | |
| } | |
| } | |
| return errors.length > 0 ? err(errors) : ok(values); | |
| } | |
| /** | |
| * Return first Ok or last Err from array of Results. | |
| * | |
| * @param results - Non-empty array of Results | |
| * @returns First Ok found or last Err if all fail | |
| * @throws Error if array is empty | |
| * | |
| * @example | |
| * ```typescript | |
| * const results = [Result.err("a"), Result.ok(42), Result.err("b")]; | |
| * const first = Result.firstOk(results); // Ok(42) | |
| * ``` | |
| */ | |
| function firstOk<T, E>(results: Result<T, E>[]): Result<T, E> { | |
| let lastErr: Result<T, E> | undefined; | |
| for (const result of results) { | |
| if (result.isOk()) { | |
| return result; | |
| } | |
| lastErr = result; | |
| } | |
| if (lastErr) { | |
| return lastErr; | |
| } | |
| throw new Error("firstOk called with empty array"); | |
| } | |
| export const Result = { | |
| ok, | |
| err, | |
| tryCatch, | |
| tryCatchAsync, | |
| all, | |
| partition, | |
| firstOk, | |
| } as const; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment