467 lines
12 KiB
TypeScript
467 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.value instanceof Ok ||
|
|
currentResult.value instanceof Err)
|
|
) {
|
|
currentResult = currentResult.value as R;
|
|
}
|
|
|
|
return 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",
|
|
),
|
|
);
|
|
}
|