import { none, 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"; type ResultJSON = { tag: "ok"; value: T } | { tag: "err"; error: E }; //#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; unwrapErr(): Option; match(ok: (value: T) => A, err: (error: E) => B): A | B; map(fn: (value: T) => U): Result; mapAsync(fn: (value: T) => Promise): ResultAsync; mapErr(fn: (err: E) => U): Result; mapErrAsync(fn: (err: E) => Promise): ResultAsync; andThen(fn: (value: T) => Result): Result; andThenAsync( fn: (value: T) => ResultAsync, ): ResultAsync; 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; toJSON(): ResultJSON; } 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; } unwrapErr(): Option { return none; } // 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); } mapAsync(fn: (value: T) => Promise): ResultAsync { return ResultAsync.fromSafePromise(fn(this.value)); } 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; } andThenAsync( fn: (value: T) => ResultAsync, ): ResultAsync { return fn(this.value); } mapErr(fn: (err: E) => U): Result { return ok(this.value); } mapErrAsync(fn: (err: E) => Promise): ResultAsync { return okAsync(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(); } toJSON(): ResultJSON { return { tag: "ok", value: this.value, }; } } 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(); } unwrapErr(): Option { return some(this.error); } 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); } mapAsync(fn: (value: T) => Promise): ResultAsync { return errAsync(this.error); } mapErr(fn: (err: E) => U): Result { return new Err(fn(this.error)); } mapErrAsync(fn: (err: E) => Promise): ResultAsync { return ResultAsync.fromPromise( new Promise(() => { throw ""; }), () => { return fn(this.error); }, ); } mapOption(fn: (value: UnwrapOption) => U): Result, E> { return err(this.error); } andThen(fn: (value: T) => Result): Result { return new Err(this.error); } andThenAsync( fn: (value: T) => ResultAsync, ): ResultAsync { return new Err(this.error).toAsync(); } 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); } toJSON(): { tag: "ok"; value: T } | { tag: "err"; error: E } { return { tag: "err", error: 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.value instanceof Ok || currentResult.value instanceof Err) ) { currentResult = currentResult.value as R; } return currentResult as FlattenResult; } export type UnwrapOption = T extends Option ? V : T; export type FlattenResult = R extends Result ? T extends Result ? FlattenResult extends Result ? Result : never : R : never; type ExtractError> = R extends Result ? E : never; type CollectedErrors[]> = ExtractError< R[number] >; function collectErrors[]>( ...results: R ): CollectedErrors[] { const errors: CollectedErrors[] = []; for (const result of results) { if (result.isErr()) { errors.push(result.error); } } return errors; } class FailedToParseResult extends Error { constructor(json: string) { super(`Failed to parse ${json} as result`); } } export function ResultFromJSON( input: string | unknown, ): Result { let data: unknown; if (typeof input === "string") { try { data = JSON.parse(input); } catch (e) { return err( new FailedToParseResult(getMessageFromError(e)), ); } } else { data = input; } if (typeof data !== "object" || data === null) { return err( new FailedToParseResult( "Expected an object but received type ${typeof data}.", ), ); } const resultObj = data as ResultJSON; if ("tag" in resultObj) { switch (resultObj.tag) { case "ok": { if ("value" in resultObj) { return ok(resultObj.value as T); } break; } case "err": { if ("error" in resultObj) { return err(resultObj.error as E); } break; } } } return err( new FailedToParseResult( "Object does not contain 'tag' and 'value' or 'error' property", ), ); }