Created
December 8, 2025 14:26
-
-
Save dmmulroy/39205560fa0cdb91f965dff63d7983f2 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 { AsyncResult, 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("try", () => { | |
| it("returns Ok on success", () => { | |
| const result = Result.try(() => 42); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("returns Err on throw with object form", () => { | |
| const result = Result.try({ | |
| try: () => { | |
| throw new Error("oops"); | |
| }, | |
| catch: (e: unknown) => (e as Error).message, | |
| }); | |
| expect(result.isErr() && result.error).toBe("oops"); | |
| }); | |
| it("uses default error handler wrapping in Error", () => { | |
| const original = new Error("oops"); | |
| const result = Result.try(() => { | |
| throw original; | |
| }); | |
| expect(result.isErr()).toBe(true); | |
| if (result.isErr()) { | |
| expect(result.error).toBeInstanceOf(Error); | |
| expect(result.error.message).toBe("Unexpected exception"); | |
| expect(result.error.cause).toBe(original); | |
| } | |
| }); | |
| }); | |
| describe("tryPromise", () => { | |
| it("returns Ok on success", async () => { | |
| const result = await Result.tryPromise(async () => 42); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("returns Err on rejection with object form", async () => { | |
| const result = await Result.tryPromise({ | |
| try: async () => { | |
| throw new Error("oops"); | |
| }, | |
| catch: (e: unknown) => (e as Error).message, | |
| }); | |
| expect(result.isErr() && result.error).toBe("oops"); | |
| }); | |
| it("uses default error handler wrapping in Error", async () => { | |
| const original = new Error("oops"); | |
| const result = await Result.tryPromise(async () => { | |
| throw original; | |
| }); | |
| expect(result.isErr()).toBe(true); | |
| if (result.isErr()) { | |
| expect(result.error).toBeInstanceOf(Error); | |
| expect(result.error.message).toBe("Unexpected exception"); | |
| expect(result.error.cause).toBe(original); | |
| } | |
| }); | |
| }); | |
| 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"); | |
| }); | |
| }); | |
| describe("AsyncResult", () => { | |
| describe("factory functions", () => { | |
| it("okAsync creates success AsyncResult", async () => { | |
| const result = await Result.okAsync(42); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("errAsync creates failure AsyncResult", async () => { | |
| const result = await Result.errAsync("fail"); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| it("tryPromise catches rejections", async () => { | |
| const result = await Result.tryPromise({ | |
| try: () => Promise.reject(new Error("oops")), | |
| catch: (e: unknown) => (e as Error).message, | |
| }); | |
| expect(result.isErr() && result.error).toBe("oops"); | |
| }); | |
| it("tryPromise wraps resolved values", async () => { | |
| const result = await Result.tryPromise({ | |
| try: () => Promise.resolve(42), | |
| catch: () => "error", | |
| }); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| }); | |
| describe("map", () => { | |
| it("transforms success value sync", async () => { | |
| const result = await Result.okAsync(2).map((x) => x * 3); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("transforms success value async", async () => { | |
| const result = await Result.okAsync(2).map(async (x) => x * 3); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("skips transform on error", async () => { | |
| const result = await Result.errAsync<string, number>("fail").map( | |
| (x) => x * 2, | |
| ); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| }); | |
| describe("mapErr", () => { | |
| it("transforms error value sync", async () => { | |
| const result = await Result.errAsync("fail").mapErr((e) => e.length); | |
| expect(result.isErr() && result.error).toBe(4); | |
| }); | |
| it("transforms error value async", async () => { | |
| const result = await Result.errAsync("fail").mapErr( | |
| async (e) => e.length, | |
| ); | |
| expect(result.isErr() && result.error).toBe(4); | |
| }); | |
| it("skips transform on success", async () => { | |
| const result = await Result.okAsync<number, string>(42).mapErr( | |
| (e) => e.length, | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| }); | |
| describe("andThen", () => { | |
| it("chains with sync Result", async () => { | |
| const result = await Result.okAsync(2).andThen((x) => Result.ok(x * 3)); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("chains with AsyncResult", async () => { | |
| const result = await Result.okAsync(2).andThen((x) => | |
| Result.okAsync(x * 3), | |
| ); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("propagates error", async () => { | |
| const result = await Result.okAsync(2).andThen(() => Result.err("fail")); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| it("short-circuits on error", async () => { | |
| let called = false; | |
| const result = await Result.errAsync<string, number>("fail").andThen( | |
| (x) => { | |
| called = true; | |
| return Result.ok(x * 2); | |
| }, | |
| ); | |
| expect(called).toBe(false); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| }); | |
| describe("orElse", () => { | |
| it("recovers from error with sync Result", async () => { | |
| const result = await Result.errAsync<string, number>("fail").orElse(() => | |
| Result.ok(42), | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("recovers from error with AsyncResult", async () => { | |
| const result = await Result.errAsync<string, number>("fail").orElse(() => | |
| Result.okAsync(42), | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("skips recovery on success", async () => { | |
| let called = false; | |
| const result = await Result.okAsync<number, string>(42).orElse(() => { | |
| called = true; | |
| return Result.ok(0); | |
| }); | |
| expect(called).toBe(false); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| }); | |
| describe("match", () => { | |
| it("calls ok handler on success", async () => { | |
| const result = await Result.okAsync(5).match({ | |
| ok: (v) => v * 2, | |
| err: () => 0, | |
| }); | |
| expect(result).toBe(10); | |
| }); | |
| it("calls err handler on failure", async () => { | |
| const result = await Result.errAsync<string, number>("fail").match({ | |
| ok: () => 0, | |
| err: (e) => e.length, | |
| }); | |
| expect(result).toBe(4); | |
| }); | |
| it("handles async handlers", async () => { | |
| const result = await Result.okAsync(5).match({ | |
| ok: async (v) => v * 2, | |
| err: async () => 0, | |
| }); | |
| expect(result).toBe(10); | |
| }); | |
| }); | |
| describe("unwrapOr", () => { | |
| it("returns value on success", async () => { | |
| const value = await Result.okAsync(42).unwrapOr(0); | |
| expect(value).toBe(42); | |
| }); | |
| it("returns default on error", async () => { | |
| const value = await Result.errAsync<string, number>("fail").unwrapOr(0); | |
| expect(value).toBe(0); | |
| }); | |
| }); | |
| describe("static array methods", () => { | |
| it("all returns Ok with all values", async () => { | |
| const result = await AsyncResult.all([ | |
| Result.okAsync(1), | |
| Result.okAsync(2), | |
| Result.okAsync(3), | |
| ]); | |
| expect(result.isOk() && result.value).toEqual([1, 2, 3]); | |
| }); | |
| it("all returns first error", async () => { | |
| const result = await AsyncResult.all([ | |
| Result.okAsync(1), | |
| Result.errAsync("first"), | |
| Result.errAsync("second"), | |
| ]); | |
| expect(result.isErr() && result.error).toBe("first"); | |
| }); | |
| it("partition returns Ok with all values", async () => { | |
| const result = await AsyncResult.partition([ | |
| Result.okAsync(1), | |
| Result.okAsync(2), | |
| ]); | |
| expect(result.isOk() && result.value).toEqual([1, 2]); | |
| }); | |
| it("partition collects all errors", async () => { | |
| const result = await AsyncResult.partition([ | |
| Result.okAsync(1), | |
| Result.errAsync("a"), | |
| Result.errAsync("b"), | |
| ]); | |
| expect(result.isErr() && result.error).toEqual(["a", "b"]); | |
| }); | |
| it("firstOk returns first success", async () => { | |
| const result = await AsyncResult.firstOk([ | |
| Result.errAsync("a"), | |
| Result.okAsync(42), | |
| Result.errAsync("b"), | |
| ]); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("firstOk returns last error when all fail", async () => { | |
| const result = await AsyncResult.firstOk([ | |
| Result.errAsync("a"), | |
| Result.errAsync("b"), | |
| Result.errAsync("c"), | |
| ]); | |
| expect(result.isErr() && result.error).toBe("c"); | |
| }); | |
| }); | |
| }); | |
| describe("sync Result async bridging", () => { | |
| it("Ok.map with async fn returns AsyncResult", async () => { | |
| const result = await Result.ok(2).map(async (x) => x * 3); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("Err.map with async fn returns AsyncResult", async () => { | |
| const result = await Result.err<string, number>("fail").map( | |
| async (x) => x * 2, | |
| ); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| it("Ok.andThen with AsyncResult returns AsyncResult", async () => { | |
| const result = await Result.ok(2).andThen((x) => Result.okAsync(x * 3)); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("Ok.andThen with Promise<Result> returns AsyncResult", async () => { | |
| const result = await Result.ok(2).andThen(async (x) => Result.ok(x * 3)); | |
| expect(result.isOk() && result.value).toBe(6); | |
| }); | |
| it("Err.andThen with async fn returns AsyncResult", async () => { | |
| const result = await Result.err<string, number>("fail").andThen(async (x) => | |
| Result.ok(x * 2), | |
| ); | |
| expect(result.isErr() && result.error).toBe("fail"); | |
| }); | |
| it("Err.orElse with AsyncResult returns AsyncResult", async () => { | |
| const result = await Result.err<string, number>("fail").orElse(() => | |
| Result.okAsync(42), | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| it("Err.orElse with Promise<Result> returns AsyncResult", async () => { | |
| const result = await Result.err<string, number>("fail").orElse(async () => | |
| Result.ok(42), | |
| ); | |
| expect(result.isOk() && result.value).toBe(42); | |
| }); | |
| }); |
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
| /** | |
| * A discriminated union representing either success (Ok) or failure (Err). | |
| * | |
| * @template A - The success value type | |
| * @template E - The error value type | |
| */ | |
| export type Result<A, E> = Ok<A, E> | Err<E, A>; | |
| // Helper types for inference | |
| type InferOk<R> = R extends Result<infer T, unknown> ? T : never; | |
| type InferErr<R> = R extends Result<unknown, infer E> ? E : never; | |
| type InferAsyncOk<R> = R extends AsyncResult<infer T, unknown> ? T : never; | |
| type InferAsyncErr<R> = R extends AsyncResult<unknown, infer E> ? E : never; | |
| /** | |
| * Success variant of Result. | |
| * | |
| * @template A - The success value type | |
| * @template E - The error type (phantom, unused at runtime) | |
| */ | |
| export class Ok<A, E = never> { | |
| readonly _tag = "Ok" as const; | |
| constructor(readonly value: A) {} | |
| /** | |
| * Type guard for Ok variant. | |
| * | |
| * @returns true | |
| */ | |
| isOk(): this is Ok<A, E> { | |
| return true; | |
| } | |
| /** | |
| * Type guard for Err variant. | |
| * | |
| * @returns false | |
| */ | |
| isErr(): this is Err<E, A> { | |
| return false; | |
| } | |
| /** | |
| * Transform the success value. | |
| * | |
| * @param fn - Transform function (sync or async) | |
| * @returns Result or AsyncResult with transformed value | |
| */ | |
| map<U>(fn: (value: A) => Promise<U>): AsyncResult<U, E>; | |
| map<U>(fn: (value: A) => U): Result<U, E>; | |
| map<U>(fn: (value: A) => U | Promise<U>): Result<U, E> | AsyncResult<U, E> { | |
| const result = fn(this.value); | |
| if (result instanceof Promise) { | |
| return new AsyncResult(result.then((v) => new Ok<U, E>(v))); | |
| } | |
| return new Ok(result); | |
| } | |
| /** | |
| * 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<A, F> { | |
| // SAFETY: E is phantom in Ok; | |
| return this as unknown as Ok<A, F>; | |
| } | |
| /** | |
| * Chain a Result-returning function on success. | |
| * | |
| * @param fn - Function returning Result, AsyncResult, or Promise<Result> | |
| * @returns Result or AsyncResult from fn | |
| */ | |
| andThen<R extends Result<unknown, unknown>>( | |
| fn: (value: A) => R, | |
| ): Result<InferOk<R>, E | InferErr<R>>; | |
| andThen<R extends AsyncResult<unknown, unknown>>( | |
| fn: (value: A) => R, | |
| ): AsyncResult<InferAsyncOk<R>, E | InferAsyncErr<R>>; | |
| andThen<U, F>(fn: (value: A) => Promise<Result<U, F>>): AsyncResult<U, E | F>; | |
| andThen( | |
| fn: ( | |
| value: A, | |
| ) => | |
| | Result<unknown, unknown> | |
| | AsyncResult<unknown, unknown> | |
| | Promise<Result<unknown, unknown>>, | |
| ): Result<unknown, unknown> | AsyncResult<unknown, unknown> { | |
| const result = fn(this.value); | |
| if (result instanceof AsyncResult) { | |
| return result; | |
| } | |
| if (result instanceof Promise) { | |
| return new AsyncResult(result); | |
| } | |
| return result; | |
| } | |
| /** | |
| * 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<R extends Result<A, unknown>>( | |
| _fn: (error: E) => R, | |
| ): Result<A, InferErr<R>>; | |
| orElse<R extends AsyncResult<A, unknown>>( | |
| _fn: (error: E) => R, | |
| ): AsyncResult<A, InferAsyncErr<R>>; | |
| orElse<F>(_fn: (error: E) => Promise<Result<A, F>>): AsyncResult<A, F>; | |
| orElse( | |
| _fn: ( | |
| error: E, | |
| ) => | |
| | Result<A, unknown> | |
| | AsyncResult<A, unknown> | |
| | Promise<Result<A, unknown>>, | |
| ): Result<A, unknown> | AsyncResult<A, unknown> { | |
| // SAFETY: E is phantom in Ok | |
| return this as unknown as Ok<A, never>; | |
| } | |
| /** | |
| * Extract success value. | |
| * | |
| * @returns The success value | |
| */ | |
| unwrap(): A { | |
| return this.value; | |
| } | |
| /** | |
| * Extract success value or return default. | |
| * | |
| * @param _defaultValue - Default value (not used) | |
| * @returns The success value | |
| */ | |
| unwrapOr(_defaultValue: A): A { | |
| 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: A) => 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 A - The success type (phantom, unused at runtime) | |
| * @template E - The error value type | |
| */ | |
| export class Err<E, A = never> { | |
| readonly _tag = "Err" as const; | |
| constructor(readonly error: E) {} | |
| /** | |
| * Type guard for Ok variant. | |
| * | |
| * @returns false | |
| */ | |
| isOk(): this is Ok<A, E> { | |
| return false; | |
| } | |
| /** | |
| * Type guard for Err variant. | |
| * | |
| * @returns true | |
| */ | |
| isErr(): this is Err<E, A> { | |
| return true; | |
| } | |
| /** | |
| * Transform the success value. No-op for Err. | |
| * | |
| * @param _fn - Transform function (not called) | |
| * @returns This Err with new success type, wrapped in AsyncResult if async fn detected | |
| */ | |
| map<U>(_fn: (value: A) => Promise<U>): AsyncResult<U, E>; | |
| map<U>(_fn: (value: A) => U): Result<U, E>; | |
| map<U>(fn: (value: A) => U | Promise<U>): Result<U, E> | AsyncResult<U, E> { | |
| // SAFETY: A is phantom in Err | |
| // Detect async function to return correct type | |
| if (fn.constructor.name === "AsyncFunction") { | |
| return new AsyncResult(Promise.resolve(this as unknown as Err<E>)); | |
| } | |
| return this as unknown as Err<E>; | |
| } | |
| /** | |
| * Transform the error value. | |
| * | |
| * @param fn - Transform function | |
| * @returns New Err with transformed error | |
| */ | |
| mapErr<F>(fn: (error: E) => F): Result<A, F> { | |
| return new Err(fn(this.error)); | |
| } | |
| /** | |
| * Chain a Result-returning function on success. No-op for Err. | |
| * | |
| * @param _fn - Function returning Result, AsyncResult, or Promise<Result> (not called) | |
| * @returns This Err, wrapped in AsyncResult if async fn detected | |
| */ | |
| andThen<R extends Result<unknown, unknown>>( | |
| _fn: (value: A) => R, | |
| ): Result<InferOk<R>, E | InferErr<R>>; | |
| andThen<R extends AsyncResult<unknown, unknown>>( | |
| _fn: (value: A) => R, | |
| ): AsyncResult<InferAsyncOk<R>, E | InferAsyncErr<R>>; | |
| andThen<U, F>( | |
| _fn: (value: A) => Promise<Result<U, F>>, | |
| ): AsyncResult<U, E | F>; | |
| andThen( | |
| fn: ( | |
| value: A, | |
| ) => | |
| | Result<unknown, unknown> | |
| | AsyncResult<unknown, unknown> | |
| | Promise<Result<unknown, unknown>>, | |
| ): Result<unknown, unknown> | AsyncResult<unknown, unknown> { | |
| // SAFETY: A is phantom in Err | |
| // Detect async function to return correct type | |
| if (fn.constructor.name === "AsyncFunction") { | |
| return new AsyncResult(Promise.resolve(this as unknown as Err<E>)); | |
| } | |
| return this as unknown as Err<E>; | |
| } | |
| /** | |
| * Recover from error with a Result-returning function. | |
| * | |
| * @param fn - Recovery function returning Result, AsyncResult, or Promise<Result> | |
| * @returns Result or AsyncResult from fn | |
| */ | |
| orElse<R extends Result<A, unknown>>( | |
| fn: (error: E) => R, | |
| ): Result<A, InferErr<R>>; | |
| orElse<R extends AsyncResult<A, unknown>>( | |
| fn: (error: E) => R, | |
| ): AsyncResult<A, InferAsyncErr<R>>; | |
| orElse<F>(fn: (error: E) => Promise<Result<A, F>>): AsyncResult<A, F>; | |
| orElse( | |
| fn: ( | |
| error: E, | |
| ) => | |
| | Result<A, unknown> | |
| | AsyncResult<A, unknown> | |
| | Promise<Result<A, unknown>>, | |
| ): Result<A, unknown> | AsyncResult<A, unknown> { | |
| const result = fn(this.error); | |
| if (result instanceof AsyncResult) { | |
| return result; | |
| } | |
| if (result instanceof Promise) { | |
| return new AsyncResult(result); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Extract success value. | |
| * | |
| * @throws Error always (Err has no success value) | |
| */ | |
| unwrap(): A { | |
| 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: A): A { | |
| 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: A) => U; err: (error: E) => U }): U { | |
| return handlers.err(this.error); | |
| } | |
| } | |
| /** | |
| * Async wrapper around Promise<Result<A, E>> with chainable operations. | |
| * | |
| * @template A - The success value type | |
| * @template E - The error value type | |
| */ | |
| export class AsyncResult<A, E> implements PromiseLike<Result<A, E>> { | |
| constructor(private readonly promise: Promise<Result<A, E>>) {} | |
| /** | |
| * PromiseLike implementation for await support. | |
| */ | |
| // biome-ignore lint/suspicious/noThenProperty: Needed for AsyncResult API/promise flattening | |
| then<TResult1 = Result<A, E>, TResult2 = never>( | |
| onfulfilled?: | |
| | ((value: Result<A, E>) => TResult1 | PromiseLike<TResult1>) | |
| | null, | |
| onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null, | |
| ): PromiseLike<TResult1 | TResult2> { | |
| return this.promise.then(onfulfilled, onrejected); | |
| } | |
| /** | |
| * Transform the success value. | |
| * | |
| * @param fn - Transform function (sync or async) | |
| * @returns New AsyncResult with transformed value | |
| */ | |
| map<B>(fn: (value: A) => B | Promise<B>): AsyncResult<B, E> { | |
| return new AsyncResult( | |
| this.promise.then(async (result) => { | |
| if (result.isOk()) { | |
| const mapped = fn(result.value); | |
| const resolved = mapped instanceof Promise ? await mapped : mapped; | |
| return new Ok<B, E>(resolved); | |
| } | |
| // SAFETY: A is phantom in Err | |
| return result as unknown as Err<E>; | |
| }), | |
| ); | |
| } | |
| /** | |
| * Transform the error value. | |
| * | |
| * @param fn - Transform function (sync or async) | |
| * @returns New AsyncResult with transformed error | |
| */ | |
| mapErr<F>(fn: (error: E) => F | Promise<F>): AsyncResult<A, F> { | |
| return new AsyncResult( | |
| this.promise.then(async (result) => { | |
| if (result.isErr()) { | |
| const mapped = fn(result.error); | |
| const resolved = mapped instanceof Promise ? await mapped : mapped; | |
| return new Err<F>(resolved); | |
| } | |
| // SAFETY: E is phantom in Ok | |
| return result as unknown as Ok<A, F>; | |
| }), | |
| ); | |
| } | |
| /** | |
| * Chain a Result-returning function on success. | |
| * | |
| * @param fn - Function returning Result or AsyncResult | |
| * @returns AsyncResult from fn | |
| */ | |
| andThen<R extends Result<unknown, unknown>>( | |
| fn: (value: A) => R, | |
| ): AsyncResult<InferOk<R>, E | InferErr<R>>; | |
| andThen<R extends AsyncResult<unknown, unknown>>( | |
| fn: (value: A) => R, | |
| ): AsyncResult<InferAsyncOk<R>, E | InferAsyncErr<R>>; | |
| andThen( | |
| fn: (value: A) => Result<unknown, unknown> | AsyncResult<unknown, unknown>, | |
| ): AsyncResult<unknown, unknown> { | |
| return new AsyncResult( | |
| this.promise.then(async (result) => { | |
| if (result.isOk()) { | |
| const next = fn(result.value); | |
| if (next instanceof AsyncResult) { | |
| return next.promise; | |
| } | |
| return next; | |
| } | |
| return result; | |
| }), | |
| ); | |
| } | |
| /** | |
| * Recover from error with a Result-returning function. | |
| * | |
| * @param fn - Recovery function returning Result or AsyncResult | |
| * @returns AsyncResult from fn | |
| */ | |
| orElse<R extends Result<A, unknown>>( | |
| fn: (error: E) => R, | |
| ): AsyncResult<A, InferErr<R>>; | |
| orElse<R extends AsyncResult<A, unknown>>( | |
| fn: (error: E) => R, | |
| ): AsyncResult<A, InferAsyncErr<R>>; | |
| orElse( | |
| fn: (error: E) => Result<A, unknown> | AsyncResult<A, unknown>, | |
| ): AsyncResult<A, unknown> { | |
| return new AsyncResult( | |
| this.promise.then(async (result) => { | |
| if (result.isErr()) { | |
| const next = fn(result.error); | |
| if (next instanceof AsyncResult) { | |
| return next.promise; | |
| } | |
| return next; | |
| } | |
| return result; | |
| }), | |
| ); | |
| } | |
| /** | |
| * Pattern match on Result. | |
| * | |
| * @param handlers - Object with ok and err handlers (sync or async) | |
| * @returns Promise of handler result | |
| */ | |
| async match<B>(handlers: { | |
| ok: (value: A) => B | Promise<B>; | |
| err: (error: E) => B | Promise<B>; | |
| }): Promise<B> { | |
| const result = await this.promise; | |
| if (result.isOk()) { | |
| return handlers.ok(result.value); | |
| } | |
| return handlers.err(result.error); | |
| } | |
| /** | |
| * Extract success value or return default. | |
| * | |
| * @param defaultValue - Default value to return on error | |
| * @returns Promise of success value or default | |
| */ | |
| async unwrapOr(defaultValue: A): Promise<A> { | |
| const result = await this.promise; | |
| return result.unwrapOr(defaultValue); | |
| } | |
| /** | |
| * Convert array of AsyncResults to AsyncResult of array. Fails on first error. | |
| * | |
| * @param results - Array of AsyncResults | |
| * @returns AsyncResult with array of values or first error | |
| */ | |
| static all<T, F>(results: AsyncResult<T, F>[]): AsyncResult<T[], F> { | |
| return new AsyncResult( | |
| Promise.all(results.map((r) => r.promise)).then((settled) => | |
| Result.all(settled), | |
| ), | |
| ); | |
| } | |
| /** | |
| * Convert array of AsyncResults to AsyncResult of array. Collects all errors. | |
| * | |
| * @param results - Array of AsyncResults | |
| * @returns AsyncResult with array of values or array of all errors | |
| */ | |
| static partition<T, F>(results: AsyncResult<T, F>[]): AsyncResult<T[], F[]> { | |
| return new AsyncResult( | |
| Promise.all(results.map((r) => r.promise)).then((settled) => | |
| Result.partition(settled), | |
| ), | |
| ); | |
| } | |
| /** | |
| * Return first Ok or last Err from array of AsyncResults. | |
| * | |
| * @param results - Non-empty array of AsyncResults | |
| * @returns AsyncResult with first Ok or last Err | |
| */ | |
| static firstOk<T, F>(results: AsyncResult<T, F>[]): AsyncResult<T, F> { | |
| return new AsyncResult( | |
| Promise.all(results.map((r) => r.promise)).then((settled) => | |
| Result.firstOk(settled), | |
| ), | |
| ); | |
| } | |
| /** | |
| * Wrap a promise-returning function, catching rejections. | |
| * | |
| * @param args - Object with try/catch handlers, or just a try function | |
| * @returns AsyncResult with resolved value or transformed error | |
| * | |
| * @example | |
| * ```typescript | |
| * // With custom error handler | |
| * const result = Result.tryPromise({ | |
| * try: () => fetch(url).then(r => r.json()), | |
| * catch: (e) => new FetchError({ cause: e }) | |
| * }); | |
| * | |
| * // Without handler (wraps in Error) | |
| * const result = Result.tryPromise(() => fetch(url)); | |
| * ``` | |
| */ | |
| static tryPromise<T, E>(handlers: { | |
| try: () => Promise<T>; | |
| catch: (cause: unknown) => E; | |
| }): AsyncResult<T, E>; | |
| static tryPromise<T, E = Error>(fn: () => Promise<T>): AsyncResult<T, E>; | |
| static tryPromise<T, E = Error>( | |
| args: | |
| | { try: () => Promise<T>; catch: (cause: unknown) => E } | |
| | (() => Promise<T>), | |
| ): AsyncResult<T, E> { | |
| if (typeof args === "object" && "try" in args) { | |
| return new AsyncResult( | |
| args | |
| .try() | |
| .then((value) => new Ok<T, E>(value)) | |
| .catch((cause) => new Err<E>(args.catch(cause))), | |
| ); | |
| } | |
| return new AsyncResult( | |
| args() | |
| .then((value) => new Ok<T, E>(value)) | |
| .catch( | |
| (cause) => | |
| new Err<E>(new Error("Unexpected exception", { cause }) as E), | |
| ), | |
| ); | |
| } | |
| } | |
| /** | |
| * 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 args - Object with try/catch handlers, or just a try function | |
| * @returns Ok with return value or Err with transformed error | |
| * | |
| * @example | |
| * ```typescript | |
| * // With custom error handler | |
| * const result = Result.try({ | |
| * try: () => JSON.parse(input), | |
| * catch: (e) => new ParseError({ cause: e }) | |
| * }); | |
| * | |
| * // Without handler (wraps in Error) | |
| * const result = Result.try(() => JSON.parse(input)); | |
| * ``` | |
| */ | |
| function tryCatch<T, E = Error>(handlers: { | |
| try: () => T; | |
| catch: (cause: unknown) => E; | |
| }): Result<T, E>; | |
| function tryCatch<T, E = Error>(fn: () => T): Result<T, E>; | |
| function tryCatch<T, E = Error>( | |
| args: { try: () => T; catch: (cause: unknown) => E } | (() => T), | |
| ): Result<T, E> { | |
| if (typeof args === "object" && "try" in args) { | |
| try { | |
| return Result.ok(args.try()); | |
| } catch (cause) { | |
| return Result.err(args.catch(cause)); | |
| } | |
| } | |
| try { | |
| return Result.ok(args()); | |
| } catch (cause) { | |
| // SAFETY: The caller did not pass a catch handler so E defaults to type of Error | |
| return Result.err(new Error("Unexpected exception", { cause })) as Result< | |
| T, | |
| 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.isOk()) { | |
| values.push(result.value); | |
| } else { | |
| // SAFETY: A is phantom in Err, casting to adjust phantom type | |
| return result as unknown as Err<E, T[]>; | |
| } | |
| } | |
| 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.isOk()) { | |
| values.push(result.value); | |
| } else { | |
| errors.push(result.error); | |
| } | |
| } | |
| 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"); | |
| } | |
| /** | |
| * Create a success AsyncResult. | |
| * | |
| * @param value - The success value | |
| * @returns AsyncResult containing Ok with value | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.okAsync(42); | |
| * ``` | |
| */ | |
| function okAsync<T, E = never>(value: T): AsyncResult<T, E> { | |
| return new AsyncResult(Promise.resolve(new Ok<T, E>(value))); | |
| } | |
| /** | |
| * Create a failure AsyncResult. | |
| * | |
| * @param error - The error value | |
| * @returns AsyncResult containing Err with error | |
| * | |
| * @example | |
| * ```typescript | |
| * const result = Result.errAsync("not found"); | |
| * ``` | |
| */ | |
| function errAsync<E, T = never>(error: E): AsyncResult<T, E> { | |
| return new AsyncResult(Promise.resolve(new Err<E>(error))); | |
| } | |
| export const Result = { | |
| ok, | |
| err, | |
| okAsync, | |
| errAsync, | |
| try: tryCatch, | |
| tryPromise: AsyncResult.tryPromise, | |
| all, | |
| partition, | |
| firstOk, | |
| } as const; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment