Keyborg/shared/utils/result.ts
2025-01-21 23:19:14 +03:00

340 lines
9.2 KiB
TypeScript

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<T, E> {
isOk(): this is Ok<T, E>;
ifOk(fn: (value: T) => void): Result<T, E>;
isErr(): this is Err<T, E>;
ifErr(fn: (err: E) => void): Result<T, E>;
isErrOrNone(): this is Err<None<T>, E>;
unwrap(): T;
unwrapOr<U>(defaultValue: U): T | U;
unwrapOrElse<U>(fn: () => U): T | U;
match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B;
map<U>(fn: (value: T) => U): Result<U, E>;
mapErr<U>(fn: (err: E) => U): Result<T, U>;
andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F>;
flatten(): FlattenResult<Result<T, E>>;
flattenOption<U>(errFn: () => U): Result<UnwrapOption<T>, U | E>;
flattenOptionOr<D = UnwrapOption<T>>(
defaultValue: D,
): Result<UnwrapOption<T> | D, E>;
mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E>;
matchOption<A, B>(
some: (value: UnwrapOption<T>) => A,
none: () => B,
): Result<A | B, E>;
toNullable(): T | null;
toAsync(): ResultAsync<T, E>;
void(): Result<void, E>;
}
export class Ok<T, E> implements IResult<T, E> {
public readonly tag = "Ok";
constructor(public readonly value: T) {
this.value = value;
Object.defineProperties(this, {
tag: {
writable: false,
enumerable: false,
},
});
}
isErr(): this is Err<T, E> {
return false;
}
ifErr(fn: (err: E) => void): Result<T, E> {
return this;
}
isErrOrNone(): this is Err<None<T>, E> {
if (this.value instanceof None) {
return true;
}
return false;
}
isOk(): this is Ok<T, E> {
return true;
}
ifOk(fn: (value: T) => void): Result<T, E> {
fn(this.value);
return this;
}
unwrap(): T {
return this.value;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
unwrapOr<U>(defaultValue: U): T {
return this.value;
}
unwrapOrElse<U>(fn: () => U): T | U {
return this.value;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B {
return ok(this.value);
}
map<U>(fn: (value: T) => U): Result<U, E> {
const mappedValue = fn(this.value);
return new Ok<U, E>(mappedValue);
}
mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E> {
if (this.value instanceof None || this.value instanceof Some) {
return ok(this.value.map(fn));
}
return ok(some(fn(this.value as UnwrapOption<T>)));
}
andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F> {
return fn(this.value) as Result<U, E | F>;
}
mapErr<U>(fn: (err: E) => U): Result<T, U> {
return new Ok<T, U>(this.value);
}
flatten(): FlattenResult<Result<T, E>> {
return flattenResult(this);
}
flattenOption<U = E>(errFn: () => U): Result<UnwrapOption<T>, E | U> {
if (this.value instanceof None || this.value instanceof Some) {
return this.value.okOrElse(errFn);
}
return new Ok<UnwrapOption<T>, E | U>(this.value as UnwrapOption<T>);
}
flattenOptionOr<D = UnwrapOption<T>>(
defaultValue: D,
): Result<UnwrapOption<T> | D, E> {
if (this.value instanceof None || this.value instanceof Some) {
return this.value.unwrapOr(defaultValue);
}
return new Ok<UnwrapOption<T> | D, E>(this.value as UnwrapOption<T>);
}
matchOption<A, B>(
some: (value: UnwrapOption<T>) => A,
none: () => B,
): Result<A | B, E> {
if (this.value instanceof None || this.value instanceof Some) {
return ok(this.value.match(some, none));
}
return ok(some(this.value as UnwrapOption<T>));
}
toNullable(): T | null {
return this.value;
}
toAsync(): ResultAsync<T, E> {
return okAsync(this.value);
}
void(): Result<void, E> {
return ok();
}
}
export class Err<T, E> implements IResult<T, E> {
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<T, E> {
return true;
}
ifErr(fn: (err: E) => void): Result<T, E> {
fn(this.error);
return this;
}
isOk(): this is Ok<T, E> {
return false;
}
ifOk(fn: (value: T) => void): Result<T, E> {
return this;
}
isErrOrNone(): this is Err<None<T>, E> {
return true;
}
unwrap(): T {
const message = `Tried to unwrap error: ${
getMessageFromError(this.error)
}`;
throw new Error(message);
}
unwrapOr<U>(defaultValue: U): U {
return defaultValue;
}
unwrapOrElse<U>(fn: () => U): T | U {
return fn();
}
match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B {
return err(this.error);
}
map<U>(fn: (value: T) => U): Result<U, E> {
return new Err<U, E>(this.error);
}
mapErr<U>(fn: (err: E) => U): Result<T, U> {
const mappedError = fn(this.error);
return new Err<T, U>(mappedError);
}
mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E> {
return err(this.error);
}
andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F> {
return new Err<U, E | F>(this.error);
}
flatten(): FlattenResult<Result<T, E>> {
return flattenResult(this);
}
flattenOption<U>(errFn: () => U): Result<UnwrapOption<T>, E | U> {
return new Err<UnwrapOption<T>, E | U>(this.error);
}
flattenOptionOr<D = UnwrapOption<T>>(
defaultValue: D,
): Result<D | UnwrapOption<T>, E> {
return new Err<UnwrapOption<T> | D, E>(this.error);
}
matchOption<A, B>(
some: (value: UnwrapOption<T>) => A,
none: () => B,
): Result<A | B, E> {
return err<A | B, E>(this.error);
}
toNullable(): T | null {
return null;
}
toAsync(): ResultAsync<T, E> {
return errAsync(this.error);
}
void(): Result<void, E> {
return err(this.error);
}
}
export type Result<T, E> = Ok<T, E> | Err<T, E>;
//#endregion
//#region Ok and Err factory functions
export function ok<T, E = never>(val: T): Ok<T, E>;
export function ok<T extends void = void, E = never>(val: void): Ok<void, E>;
export function ok<T, E = never>(val: T): Ok<T, E> {
return new Ok(val) as Ok<T, E>;
}
export function err<T = never, E extends string = string>(err: E): Err<T, E>;
export function err<T = never, E = unknown>(err: E): Err<T, E>;
export function err<T = never, E extends void = void>(err: void): Err<T, void>;
export function err<T, E>(err: E): Err<T, E> {
return new Err(err) as Err<T, E>;
}
//#endregion
export function fromThrowable<Fn extends (...args: readonly any[]) => any, E>(
fn: Fn,
errorMapper?: (e: unknown) => E,
): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, 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<R extends Result<any, any>>(
nestedResult: R,
): FlattenResult<R> {
let currentResult = nestedResult;
while (currentResult instanceof Ok) {
currentResult = currentResult.value;
}
return currentResult as FlattenResult<R>;
}
export function ResultFromJSON<T = unknown, E = unknown>(
str: string,
): Result<T, E> {
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> = T extends Option<infer V> ? V : T;
export type FlattenResult<R> = R extends Result<infer T, infer E>
? T extends Result<any, any>
? FlattenResult<T> extends Result<infer V, infer innerE>
? Result<V, E | innerE>
: never
: R
: never;