import { some } from "@shared/utils/option.ts"; import { None, type Option, Some } from "@shared/utils/option.ts"; import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; //#region Ok, Err and Result interface IResult { isOk(): this is Ok; ifOk(fn: (value: T) => void): Result; isErr(): this is Err; ifErr(fn: (err: E) => void): Result; isErrOrNone(): this is Err, E>; unwrap(): T; unwrapOr(defaultValue: U): T | U; unwrapOrElse(fn: () => U): T | U; match(ok: (value: T) => A, err: (error: E) => B): A | B; map(fn: (value: T) => U): Result; mapErr(fn: (err: E) => U): Result; andThen(fn: (value: T) => Result): Result; flatten(): FlattenResult>; flattenOption(errFn: () => U): Result, U | E>; flattenOptionOr>( defaultValue: D, ): Result | D, E>; mapOption(fn: (value: UnwrapOption) => U): Result, E>; matchOption( some: (value: UnwrapOption) => A, none: () => B, ): Result; toNullable(): T | null; toAsync(): ResultAsync; void(): Result; } export class Ok implements IResult { public readonly tag = "Ok"; constructor(public readonly value: T) { this.value = value; Object.defineProperties(this, { tag: { writable: false, enumerable: false, }, }); } isErr(): this is Err { return false; } ifErr(fn: (err: E) => void): Result { return this; } isErrOrNone(): this is Err, E> { if (this.value instanceof None) { return true; } return false; } isOk(): this is Ok { return true; } ifOk(fn: (value: T) => void): Result { fn(this.value); return this; } unwrap(): T { return this.value; } // eslint-disable-next-line @typescript-eslint/no-unused-vars unwrapOr(defaultValue: U): T { return this.value; } unwrapOrElse(fn: () => U): T | U { return this.value; } // eslint-disable-next-line @typescript-eslint/no-unused-vars match(ok: (value: T) => A, err: (error: E) => B): A | B { return ok(this.value); } map(fn: (value: T) => U): Result { const mappedValue = fn(this.value); return new Ok(mappedValue); } mapOption(fn: (value: UnwrapOption) => U): Result, E> { if (this.value instanceof None || this.value instanceof Some) { return ok(this.value.map(fn)); } return ok(some(fn(this.value as UnwrapOption))); } andThen(fn: (value: T) => Result): Result { return fn(this.value) as Result; } mapErr(fn: (err: E) => U): Result { return new Ok(this.value); } flatten(): FlattenResult> { return flattenResult(this); } flattenOption(errFn: () => U): Result, E | U> { if (this.value instanceof None || this.value instanceof Some) { return this.value.okOrElse(errFn); } return new Ok, E | U>(this.value as UnwrapOption); } flattenOptionOr>( defaultValue: D, ): Result | D, E> { if (this.value instanceof None || this.value instanceof Some) { return this.value.unwrapOr(defaultValue); } return new Ok | D, E>(this.value as UnwrapOption); } matchOption( some: (value: UnwrapOption) => A, none: () => B, ): Result { if (this.value instanceof None || this.value instanceof Some) { return ok(this.value.match(some, none)); } return ok(some(this.value as UnwrapOption)); } toNullable(): T | null { return this.value; } toAsync(): ResultAsync { return okAsync(this.value); } void(): Result { return ok(); } } export class Err implements IResult { public readonly tag = "Err"; constructor(public readonly error: E) { this.error = error; Object.defineProperties(this, { tag: { writable: false, configurable: false, enumerable: false, }, }); } isErr(): this is Err { return true; } ifErr(fn: (err: E) => void): Result { fn(this.error); return this; } isOk(): this is Ok { return false; } ifOk(fn: (value: T) => void): Result { return this; } isErrOrNone(): this is Err, E> { return true; } unwrap(): T { const message = `Tried to unwrap error: ${ getMessageFromError(this.error) }`; throw new Error(message); } unwrapOr(defaultValue: U): U { return defaultValue; } unwrapOrElse(fn: () => U): T | U { return fn(); } match(ok: (value: T) => A, err: (error: E) => B): A | B { return err(this.error); } map(fn: (value: T) => U): Result { return new Err(this.error); } mapErr(fn: (err: E) => U): Result { const mappedError = fn(this.error); return new Err(mappedError); } mapOption(fn: (value: UnwrapOption) => U): Result, E> { return err(this.error); } andThen(fn: (value: T) => Result): Result { return new Err(this.error); } flatten(): FlattenResult> { return flattenResult(this); } flattenOption(errFn: () => U): Result, E | U> { return new Err, E | U>(this.error); } flattenOptionOr>( defaultValue: D, ): Result, E> { return new Err | D, E>(this.error); } matchOption( some: (value: UnwrapOption) => A, none: () => B, ): Result { return err(this.error); } toNullable(): T | null { return null; } toAsync(): ResultAsync { return errAsync(this.error); } void(): Result { return err(this.error); } } export type Result = Ok | Err; //#endregion //#region Ok and Err factory functions export function ok(val: T): Ok; export function ok(val: void): Ok; export function ok(val: T): Ok { return new Ok(val) as Ok; } export function err(err: E): Err; export function err(err: E): Err; export function err(err: void): Err; export function err(err: E): Err { return new Err(err) as Err; } //#endregion export function fromThrowable any, E>( fn: Fn, errorMapper?: (e: unknown) => E, ): (...args: Parameters) => Result, E> { return (...args) => { try { const result = fn(...args); return ok(result); } catch (e) { return err(errorMapper ? errorMapper(e) : e); } }; } /** * utility function to get an error message from an thrown unknown type */ export function getMessageFromError(e: unknown): string { if (e instanceof Error) { if (e.message) { return e.message; } if ("code" in e && typeof e.code === "string") { return e.code; } return "An unknown error occurred"; } if (typeof e === "string") { return e; } if (typeof e === "object" && e !== null && "message" in e) { // If e is an object with a message property (could be a custom error-like object) const obj = e as { message: unknown }; return typeof obj.message === "string" ? obj.message : String(obj.message); } return "An unknown error occurred"; } export function flattenResult>( nestedResult: R, ): FlattenResult { let currentResult = nestedResult; while (currentResult instanceof Ok) { currentResult = currentResult.value; } return currentResult as FlattenResult; } export function ResultFromJSON( str: string, ): Result { const result: { value: T } | { error: E } = JSON.parse(str); if (obj.value) { return ok(obj.value); } if (obj.error) { return err(obj.error); } } export type UnwrapOption = T extends Option ? V : T; export type FlattenResult = R extends Result ? T extends Result ? FlattenResult extends Result ? Result : never : R : never;