From 6811cf421f0b4bcceb0eae4df9b947ae6730f634 Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Mon, 25 May 2026 09:45:19 -0400 Subject: [PATCH] feat: don't constrain error types --- src/errors.ts | 21 ++--------- src/option.ts | 19 ++++------ src/result.ts | 85 ++++++++++++++------------------------------ tests/result.test.ts | 30 +++------------- 4 files changed, 40 insertions(+), 115 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 4edfc26..86b36d3 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,8 +1,6 @@ -import { type ResultError } from './result'; - export class PanicError extends Error { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'PanicError'; } } @@ -30,18 +28,3 @@ export function assertValueIsNotMissing( message || 'Expected a non-null, non-undefined value' ); } - -export function assertIsResultError( - error: unknown -): asserts error is ResultError { - assertValueIsNotMissing(error, 'Expected an error object'); - - if ( - typeof error !== 'object' || - error === null || - typeof (error as any).code !== 'string' - ) - throw new InvalidArgumentError( - "Expected an object with a string 'code' property" - ); -} diff --git a/src/option.ts b/src/option.ts index 4141b2e..fe2dc5f 100644 --- a/src/option.ts +++ b/src/option.ts @@ -1,11 +1,6 @@ -import { - assertIsResultError, - FlattenError, - InvalidArgumentError, - PanicError -} from './errors'; +import { FlattenError, InvalidArgumentError, PanicError } from './errors'; import { type Either, Left, Right, isLeft, isRight } from './either'; -import { type Result, type ResultError, Ok, Err } from './result'; +import { type Result, Ok, Err } from './result'; /** * Represents some value of type `T`. @@ -116,14 +111,14 @@ interface OptionMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - okOr(err: E): Result; + okOr(err: E): Result; /** * Transforms the `Option` into a `Result`, mapping `Some(v)` to `Ok(v)` and `None` to `Err(err())`. * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - okOrElse(errF: () => E): Result; + okOrElse(errF: () => E): Result; /** * Returns an iterator over the possibly contained value. @@ -351,15 +346,13 @@ class OptionImpl implements OptionMethods { return isRight(state) ? f(state.right) : defaultF(); } - okOr(err: E): Result { - assertIsResultError(err); - + okOr(err: E): Result { const state = this.#state; if (isRight(state)) return Ok(state.right); return Err(err); } - okOrElse(errF: () => E): Result { + okOrElse(errF: () => E): Result { if (typeof errF !== 'function') throw new InvalidArgumentError('Argument must be a function'); diff --git a/src/result.ts b/src/result.ts index 63af463..5e1e063 100644 --- a/src/result.ts +++ b/src/result.ts @@ -1,31 +1,18 @@ -import { - assertIsResultError, - FlattenError, - InvalidArgumentError, - PanicError -} from './errors'; +import { FlattenError, InvalidArgumentError, PanicError } from './errors'; import { type Either, Left, Right, isLeft, isRight } from './either'; import { type Option, Some, None } from './option'; -/** - * The base interface for all errors returned by a `Result`. - * It requires a `code` property which can be used to identify the error type. - */ -export interface ResultError { - code: string; -} - /** * Represents a successful `Result` containing a value of type `T`. */ -export type OkResult = ResultMethods & { +export type OkResult = ResultMethods & { readonly _isOk: true; }; /** * Represents a failed `Result` containing an error of type `E`. */ -export type ErrResult = ResultMethods & { +export type ErrResult = ResultMethods & { readonly _isOk: false; }; @@ -37,14 +24,12 @@ export type ErrResult = ResultMethods & { * * Functions return `Result` whenever errors are expected and recoverable. * - * The error type `E` must extend `ResultError` which contains a `code` property of type `string`. - * * @template T - Contains the success value. - * @template E - Contains the error value. Must have a `code` property of type `string`. + * @template E - Contains the error value. */ -export type Result = OkResult | ErrResult; +export type Result = OkResult | ErrResult; -interface ResultMethods { +interface ResultMethods { toString(): string; /** @@ -121,7 +106,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - mapErr(f: (err: E) => F): Result; + mapErr(f: (err: E) => F): Result; /** * Calls a function with a reference to the contained value if `Ok`. @@ -185,7 +170,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - and(res: Result): Result; + and(res: Result): Result; /** * Calls `f` if the result is `Ok`, otherwise returns the `Err` value of `self`. @@ -194,9 +179,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - andThen( - f: (val: T) => Result - ): Result; + andThen(f: (val: T) => Result): Result; /** * Returns `res` if the result is `Err`, otherwise returns the `Ok` value of `self`. @@ -205,7 +188,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - or(res: Result): Result; + or(res: Result): Result; /** * Calls `f` if the result is `Err`, otherwise returns the `Ok` value of `self`. @@ -214,9 +197,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - orElse( - f: (err: E) => Result - ): Result; + orElse(f: (err: E) => Result): Result; /** * Returns the contained `Ok` value or a provided default. @@ -239,9 +220,7 @@ interface ResultMethods { * * @throws If this method throws an error other than a panic, it indicates misuse of the library (garbage data, bypass of the type system, or invalid runtime input). Check your code. */ - flatten( - this: Result, E> - ): Result; + flatten(this: Result, E>): Result; /** * Matches the `Result` with two functions, one for each variant. @@ -251,7 +230,7 @@ interface ResultMethods { match(handlers: { Ok: (val: T) => U; Err: (err: E) => U }): U; } -class ResultImpl implements ResultMethods { +class ResultImpl implements ResultMethods { // will error at runtime if trying to access # fields #state: Either; @@ -267,13 +246,13 @@ class ResultImpl implements ResultMethods { get [Symbol.toStringTag]() { const state = this.#state; if (isRight(state)) return `Result Ok`; - return `Result Err(${state.left.code})`; + return `Result Err`; } toString(): string { const state = this.#state; if (isRight(state)) return `Ok(${state.right})`; - return `Err(${state.left.code})`; + return `Err(${state.left})`; } isOk(): this is OkResult { @@ -349,7 +328,7 @@ class ResultImpl implements ResultMethods { return isLeft(state) ? fallbackFn(state.left) : f(state.right); } - mapErr(f: (err: E) => F): Result { + mapErr(f: (err: E) => F): Result { if (typeof f !== 'function') throw new InvalidArgumentError('Argument must be a function'); @@ -387,8 +366,7 @@ class ResultImpl implements ResultMethods { throw new InvalidArgumentError('Argument must be a string'); const state = this.#state; - if (isLeft(state)) - throw new PanicError(`${msg}: code "${state.left.code}"`); + if (isLeft(state)) throw new PanicError(msg, { cause: state.left }); return state.right; } @@ -396,7 +374,8 @@ class ResultImpl implements ResultMethods { const state = this.#state; if (isLeft(state)) throw new PanicError( - `called \`Result.unwrap()\` on an \`Err\` value: code "${state.left.code}"` + `called \`Result.unwrap()\` on an \`Err\` value`, + { cause: state.left } ); return state.right; } @@ -420,7 +399,7 @@ class ResultImpl implements ResultMethods { return state.left; } - and(res: Result): Result { + and(res: Result): Result { if (!(res instanceof ResultImpl)) throw new InvalidArgumentError('Argument must be a Result'); @@ -429,9 +408,7 @@ class ResultImpl implements ResultMethods { return new ResultImpl(Left(state.left)); } - andThen( - f: (val: T) => Result - ): Result { + andThen(f: (val: T) => Result): Result { if (typeof f !== 'function') throw new InvalidArgumentError('Argument must be a function'); @@ -440,7 +417,7 @@ class ResultImpl implements ResultMethods { return new ResultImpl(Left(state.left)); } - or(res: Result): Result { + or(res: Result): Result { if (!(res instanceof ResultImpl)) throw new InvalidArgumentError('Argument must be a Result'); @@ -449,9 +426,7 @@ class ResultImpl implements ResultMethods { return new ResultImpl(Right(state.right)); } - orElse( - f: (err: E) => Result - ): Result { + orElse(f: (err: E) => Result): Result { if (typeof f !== 'function') throw new InvalidArgumentError('Argument must be a function'); @@ -473,9 +448,7 @@ class ResultImpl implements ResultMethods { return isLeft(state) ? f(state.left) : state.right; } - flatten( - this: Result, E> - ): Result { + flatten(this: Result, E>): Result { const _this = this as ResultImpl, E>; const state = _this.#state; @@ -513,20 +486,16 @@ class ResultImpl implements ResultMethods { * @param value - The value to wrap in a successful result. * @returns A `Result` representing a successful outcome. */ -export function Ok(value: T): Result { +export function Ok(value: T): Result { return new ResultImpl(Right(value)); } /** * Contains the error value. * - * @param error - The error to wrap in a failed result. Must have a `code` property of type `string`. + * @param error - The error to wrap in a failed result. * @returns A `Result` representing a failed outcome. */ -export function Err< - const C extends string, - E extends ResultError & { code: C } ->(error: E): Result { - assertIsResultError(error); +export function Err(error: E): Result { return new ResultImpl(Left(error)); } diff --git a/tests/result.test.ts b/tests/result.test.ts index ad09f6a..ed83dd7 100644 --- a/tests/result.test.ts +++ b/tests/result.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; import { Ok, Err } from '../src/result'; -import { FlattenError, InvalidArgumentError, PanicError } from '../src/errors'; +import { FlattenError, PanicError } from '../src/errors'; describe('Result', () => { test('construction', () => { @@ -11,26 +11,6 @@ describe('Result', () => { const okUndefined = Ok(undefined); expect(okUndefined.unwrap()).toBeUndefined(); - // @ts-expect-error - error object must be passed - expect(() => Err(null)).toThrow( - new InvalidArgumentError('Expected an error object') - ); - // @ts-expect-error - error object must be passed - expect(() => Err(undefined)).toThrow( - new InvalidArgumentError('Expected an error object') - ); - // @ts-expect-error - code property must be a string - expect(() => Err({ code: null })).toThrow( - new InvalidArgumentError( - "Expected an object with a string 'code' property" - ) - ); - // @ts-expect-error - code property must be a string - expect(() => Err({ code: undefined })).toThrow( - new InvalidArgumentError( - "Expected an object with a string 'code' property" - ) - ); }); test('isOk', () => { expect(Ok(5).isOk()).toBe(true); @@ -155,16 +135,16 @@ describe('Result', () => { test('expect', () => { expect(Ok(5).expect('Should not fail')).toBe(5); expect(() => Err({ code: 'ERR' }).expect('Failed')).toThrow( - new PanicError('Failed: code "ERR"') + new PanicError('Failed', { cause: { code: 'ERR' } }) ); }); test('unwrap', () => { expect(Ok(5).unwrap()).toBe(5); expect(() => Err({ code: 'ERR' }).unwrap()).toThrow( - new PanicError( - 'called `Result.unwrap()` on an `Err` value: code "ERR"' - ) + new PanicError('called `Result.unwrap()` on an `Err` value', { + cause: { code: 'ERR' } + }) ); });