@jumpaku/async-result
Version:
A typescript library that provides Result, Option, and AsyncResult.
306 lines (299 loc) • 9.72 kB
text/typescript
import { BaseError } from "make-error-cause";
import { AsyncResult } from "./AsyncResult";
export class ResultError<E> extends BaseError {
readonly name: string = "ResultError";
readonly detail: E;
constructor(error: E) {
super(
error instanceof Error ? error.message : "",
error instanceof Error ? error : undefined
);
this.detail = error;
}
}
interface ResultTry {
<V>(tryFun: () => V): Result<V, unknown>;
<V, E>(tryFun: () => V, catchFun: (error: unknown) => E): Result<V, E>;
}
export const Result: {
success: <V>(value: V) => Result<V, never>;
failure: <E>(error: E) => Result<never, E>;
try: ResultTry;
isResult: <V, E>(obj: unknown) => obj is Result<V, E>;
} = {
success<V>(value: V): Result<V, never> {
return new Success<V, never>(value);
},
failure<E>(error: E): Result<never, E> {
return new Failure<never, E>(error);
},
try: <V, E>(
tryFun: () => V,
catchFun?: (error: unknown) => E
): Result<V, unknown> | Result<V, E> => {
try {
return Result.success(tryFun());
} catch (error: unknown) {
return catchFun == null
? Result.failure(error)
: Result.failure(catchFun(error));
}
},
isResult<V, E>(obj: unknown): obj is Result<V, E> {
return obj instanceof AbstractResult;
},
};
abstract class AbstractResult<V, E> {
private assertsThisIsResult(): asserts this is Result<V, E> {
if (!this.isSuccess() && !this.isFailure()) throw new Error();
}
abstract isSuccess(): this is Success<V, E>;
abstract isFailure(): this is Failure<V, E>;
match<X, Y>(onSuccess: (value: V) => X, onFailure: (error: E) => Y): X | Y {
this.assertsThisIsResult();
return this.isSuccess() ? onSuccess(this.value) : onFailure(this.error);
}
orDefault(value: V): V {
return this.isSuccess() ? this.value : value;
}
orRecover(f: (error: E) => V): V {
this.assertsThisIsResult();
return this.isSuccess() ? this.value : f(this.error);
}
orThrow<F>(f?: (e: E) => F): V {
this.assertsThisIsResult();
if (this.isFailure())
throw f != null ? f(this.error) : new ResultError(this.error);
return this.value;
}
orNull(): V | null {
return this.isSuccess() ? this.value : null;
}
orUndefined(): V | undefined {
return this.isSuccess() ? this.value : undefined;
}
onSuccess(f: (value: V) => void): Result<V, E> {
if (this.isSuccess()) f(this.value);
this.assertsThisIsResult();
return this;
}
onFailure(f: (error: E) => void): Result<V, E> {
this.assertsThisIsResult();
if (this.isFailure()) f(this.error);
return this;
}
and<U, F>(other: Result<U, F>): Result<V | U, E | F> {
return this.isFailure() ? this : other;
}
or<U, F>(other: Result<U, F>): Result<V | U, E | F> {
return this.isSuccess() ? this : other;
}
map<U>(neverThrowFun: (value: V) => U): Result<U, E> {
this.assertsThisIsResult();
return this.isSuccess()
? Result.success(neverThrowFun(this.value))
: this.castValue();
}
mapAsync<U>(tryFun: (value: V) => U | Promise<U>): AsyncResult<U, unknown>;
mapAsync<U, F>(
tryFun: (value: V) => U | Promise<U>,
catchFun: (error: unknown) => F
): AsyncResult<U, F>;
mapAsync<U, F>(
tryFun: (value: V) => U | Promise<U>,
catchFun?: (error: unknown) => F
): AsyncResult<U, F | unknown> {
return catchFun == null
? this.match(
(v) => AsyncResult.try(() => tryFun(v)),
(e) => AsyncResult.failure(e)
)
: this.match(
(v) => AsyncResult.try(() => tryFun(v), catchFun),
(e) => AsyncResult.failure(e)
);
}
tryMap<U>(tryFun: (value: V) => U): Result<U, unknown>;
tryMap<U, F>(
tryFun: (value: V) => U,
catchFun: (error: unknown) => F
): Result<U, E | F>;
tryMap<U, F>(
tryFun: (value: V) => U,
catchFun?: (error: unknown) => F
): Result<U, E | F | unknown> {
return (catchFun == null
? Result.try(() => this.map(tryFun))
: Result.try(() => this.map(tryFun), catchFun)
).flatMap((it) => it);
}
flatMap<U, F>(neverThrowFun: (value: V) => Result<U, F>): Result<U, E | F> {
this.assertsThisIsResult();
return this.isSuccess() ? neverThrowFun(this.value) : this.castValue();
}
flatMapAsync<U, F>(
tryFun: (value: V) => Result<U, F> | AsyncResult<U, F>
): AsyncResult<U, unknown>;
flatMapAsync<U, F, G>(
tryFun: (value: V) => Result<U, F> | AsyncResult<U, F>,
catchFun: (error: unknown) => G
): AsyncResult<U, E | F | G>;
flatMapAsync<U, F, G>(
tryFun: (value: V) => Result<U, F> | AsyncResult<U, F>,
catchFun?: (error: unknown) => G
): AsyncResult<U, E | F | G | unknown> {
this.assertsThisIsResult();
if (this.isFailure()) return AsyncResult.of(this.castValue());
try {
const result = tryFun(this.value);
return Result.isResult(result) ? AsyncResult.of(result) : result;
} catch (error: unknown) {
return catchFun == null
? AsyncResult.failure(error)
: AsyncResult.failure(catchFun(error));
}
}
tryFlatMap<U, F>(tryFun: (value: V) => Result<U, F>): Result<U, unknown>;
tryFlatMap<U, F, G>(
tryFun: (value: V) => Result<U, F>,
catchFun: (error: unknown) => G
): Result<U, E | F | G>;
tryFlatMap<U, F, G>(
tryFun: (value: V) => Result<U, F>,
catchFun?: (error: unknown) => G
): Result<U, E | F | G | unknown> {
return (catchFun == null
? Result.try(() => this.flatMap(tryFun))
: Result.try(() => this.flatMap(tryFun), catchFun)
).flatMap((it) => it);
}
recover(neverThrowFun: (error: E) => V): Result<V, never> {
this.assertsThisIsResult();
return this.isFailure()
? Result.success(neverThrowFun(this.error))
: this.castError();
}
recoverAsync<F>(
tryFun: (error: E) => V | Promise<V>
): AsyncResult<V, unknown>;
recoverAsync<F>(
tryFun: (error: E) => V | Promise<V>,
catchFun: (error: unknown) => F
): AsyncResult<V, F>;
recoverAsync<F>(
tryFun: (error: E) => V | Promise<V>,
catchFun?: (error: unknown) => F
): AsyncResult<V, F | unknown> {
this.assertsThisIsResult();
const f = async () => {
this.assertsThisIsResult();
return this.isSuccess() ? this.value : tryFun(this.error);
};
return catchFun == null ? AsyncResult.try(f) : AsyncResult.try(f, catchFun);
}
tryRecover(tryFun: (error: E) => V): Result<V, unknown>;
tryRecover<F>(
tryFun: (error: E) => V,
catchFun: (error: unknown) => F
): Result<V, F>;
tryRecover<F>(
tryFun: (error: E) => V,
catchFun?: (error: unknown) => F
): Result<V, F | unknown> {
return (catchFun == null
? Result.try(() => this.recover(tryFun))
: Result.try(() => this.recover(tryFun), catchFun)
).flatMap((it) => it);
}
flatRecover<F>(neverThrowFun: (error: E) => Result<V, F>): Result<V, F> {
this.assertsThisIsResult();
return this.isFailure() ? neverThrowFun(this.error) : this.castError();
}
flatRecoverAsync<F>(
tryFun: (error: E) => Result<V, F> | AsyncResult<V, F>
): AsyncResult<V, unknown>;
flatRecoverAsync<F, G>(
tryFun: (error: E) => Result<V, F> | AsyncResult<V, F>,
catchFun: (error: unknown) => G
): AsyncResult<V, F | G>;
flatRecoverAsync<F, G>(
tryFun: (error: E) => Result<V, F> | AsyncResult<V, F>,
catchFun?: (error: unknown) => G
): AsyncResult<V, F | G | unknown> {
this.assertsThisIsResult();
if (this.isSuccess()) return AsyncResult.of(this);
try {
const result = tryFun(this.error);
return Result.isResult(result) ? AsyncResult.of(result) : result;
} catch (error: unknown) {
return catchFun == null
? AsyncResult.failure(error)
: AsyncResult.failure(catchFun(error));
}
}
tryFlatRecover<F>(tryFun: (error: E) => Result<V, F>): Result<V, unknown>;
tryFlatRecover<F, G>(
tryFun: (error: E) => Result<V, F>,
catchFun: (error: unknown) => G
): Result<V, F | G>;
tryFlatRecover<F, G>(
tryFun: (error: E) => Result<V, F>,
catchFun?: (error: unknown) => G
): Result<V, F | G | unknown> {
return (catchFun == null
? Result.try(() => this.flatRecover(tryFun))
: Result.try(() => this.flatRecover(tryFun), catchFun)
).flatMap((it) => it);
}
mapError<F>(neverThrowFun: (error: E) => F): Result<V, F> {
this.assertsThisIsResult();
return this.isFailure()
? Result.failure(neverThrowFun(this.error))
: this.castError();
}
tryMapError<F>(tryFun: (error: E) => F): Result<V, unknown>;
tryMapError<F, G>(
tryFun: (error: E) => F,
catchFun: (error: unknown) => G
): Result<V, F | G>;
tryMapError<F, G>(
tryFun: (error: E) => F,
catchFun?: (error: unknown) => G
): Result<V, F | G | unknown> {
return (catchFun == null
? Result.try(() => this.mapError(tryFun))
: Result.try(() => this.mapError(tryFun), catchFun)
).flatMap((it) => it);
}
}
export class Success<V, E> extends AbstractResult<V, E> {
constructor(readonly value: V) {
super();
}
readonly error = undefined;
isSuccess(): this is Success<V, E> {
return true;
}
isFailure(): this is Failure<V, E> {
return false;
}
castError<F>(): Success<V, F> {
return (this as any) as Success<V, F>;
}
}
export class Failure<V, E> extends AbstractResult<V, E> {
constructor(readonly error: E) {
super();
}
readonly value = undefined;
isSuccess(): this is Success<V, E> {
return false;
}
isFailure(): this is Failure<V, E> {
return true;
}
castValue<U>(): Failure<U, E> {
return (this as any) as Failure<U, E>;
}
}
export type Result<V, E> = Success<V, E> | Failure<V, E>;