Keyborg/shared/utils/result.ts

463 lines
12 KiB
TypeScript

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<T, E> = { tag: "ok"; value: T } | { tag: "err"; error: E };
//#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;
unwrapErr(): Option<E>;
match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B;
map<U>(fn: (value: T) => U): Result<U, E>;
mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E>;
mapErr<U>(fn: (err: E) => U): Result<T, U>;
mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U>;
andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F>;
andThenAsync<U, F>(
fn: (value: T) => ResultAsync<U, F>,
): ResultAsync<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>;
toJSON(): ResultJSON<T, 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;
}
unwrapErr(): Option<E> {
return none;
}
// 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);
}
mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E> {
return ResultAsync.fromSafePromise(fn(this.value));
}
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>;
}
andThenAsync<U, F>(
fn: (value: T) => ResultAsync<U, F>,
): ResultAsync<U, E | F> {
return fn(this.value);
}
mapErr<U>(fn: (err: E) => U): Result<T, U> {
return ok<T, U>(this.value);
}
mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U> {
return okAsync(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();
}
toJSON(): ResultJSON<T, E> {
return {
tag: "ok",
value: this.value,
};
}
}
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();
}
unwrapErr(): Option<E> {
return some(this.error);
}
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);
}
mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E> {
return errAsync(this.error);
}
mapErr<U>(fn: (err: E) => U): Result<T, U> {
return new Err<T, U>(fn(this.error));
}
mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U> {
return ResultAsync.fromPromise(
new Promise(() => {
throw "";
}),
() => {
return fn(this.error);
},
);
}
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);
}
andThenAsync<U, F>(
fn: (value: T) => ResultAsync<U, F>,
): ResultAsync<U, E | F> {
return new Err<U, E | F>(this.error).toAsync();
}
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);
}
toJSON(): { tag: "ok"; value: T } | { tag: "err"; error: E } {
return {
tag: "err",
error: 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 ok(currentResult) as FlattenResult<R>;
}
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;
type ExtractError<R extends Result<unknown, unknown>> = R extends
Result<unknown, infer E> ? E : never;
type CollectedErrors<R extends Result<unknown, unknown>[]> = ExtractError<
R[number]
>;
function collectErrors<R extends Result<any, any>[]>(
...results: R
): CollectedErrors<R>[] {
const errors: CollectedErrors<R>[] = [];
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<T = unknown, E = unknown>(
input: string | unknown,
): Result<T, E | FailedToParseResult> {
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<T, E>;
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",
),
);
}