/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ /* eslint-disable @typescript-eslint/no-namespace */ function isAsyncFn(fn: Function) { return fn.constructor.name === "AsyncFunction"; } function isResult(value: unknown): value is Result { return value instanceof Ok || value instanceof Err; } interface SyncThenable { isSync: true; then Promise>(cb: Fn): ReturnType; then any>(cb: Fn): SyncThenable; } function syncThenable(): SyncThenable { function then Promise>(cb: Fn): ReturnType; function then any>(cb: Fn): SyncThenable; function then(cb: any) { const result = cb(); if (result instanceof Promise) { return result; } return syncThenable(); } return { isSync: true, then, }; } function forEachValueThunkOrPromise(items: unknown[], execFn: (value: T) => boolean, foldFn: () => unknown) { let shouldBreak = false; const result: any = items.reduce((prev: { then: Function }, valueOrThunk) => { return prev.then(() => { if (shouldBreak) { return null; } function run(value: T) { const isSuccess = execFn(value); if (!isSuccess) { shouldBreak = true; } } const valueOrPromise = typeof valueOrThunk === "function" ? valueOrThunk() : valueOrThunk; if (valueOrPromise instanceof Promise) { return valueOrPromise.then(run); } return run(valueOrPromise); }); }, syncThenable()); if ((result as SyncThenable).isSync) { return foldFn(); } return result.then(() => { return foldFn(); }); } export type Result = | Ok | Err; interface IResult { isSuccess(): this is Ok; isFailure(): this is Err; getOrNull(): OkType | null; toString(): string; inspect(): string; fold(onSuccess: (value: OkType) => R, onFailure: (error: ErrorType) => R): R; fold(onSuccess: (value: OkType) => Promise, onFailure: (error: ErrorType) => Promise): Promise; getOrDefault(defaultValue: OkType): OkType; getOrElse(onFailure: (error: ErrorType) => OkType): OkType; getOrElse(onFailure: (error: ErrorType) => Promise): Promise; getOrThrow(): OkType; map( fn: (value: OkType) => Promise ): Promise ? T : Result>>; map( fn: (value: OkType) => T ): JoinErrorTypes ? T : Result>; rollback(): Result | Promise>; } type InferErrorType> = T extends Result ? Errortype : never; type InferOkType> = T extends Result ? OkType : never; type JoinErrorTypes> = Result< ErrorType | InferErrorType, InferOkType, any >; type ExtractErrorTypes = { [Index in keyof Tuple]: Tuple[Index] extends Result ? InferErrorType : never; }[number]; type MapResultTupleToOkTypeTuple = { [Index in keyof Tuple]: Tuple[Index] extends Result ? InferOkType : never; }; type RollbackFunction = (() => void) | (() => Promise); type HasAsyncRollbackFunction = { [Index in keyof T]: T[Index] extends () => Promise | infer U ? U extends Result Promise> ? true : false : false; }[number] extends false ? false : true; type UnwrapThunks = { [Index in keyof T]: T[Index] extends () => Promise ? U : T[Index] extends () => infer U ? U : T[Index]; }; type HasAsyncThunk = { [Index in keyof T]: T[Index] extends () => Promise ? true : false; }[number] extends false ? false : true; type PromiseReturnType any> = T extends (...args: any) => Promise ? U : never; export namespace Result { export function ok( value?: OkType, rollbackFn?: RollbackFn ): Result { return new Ok(value || null!, rollbackFn) as any; } export function error( error: ErrorType, rollbackFn?: RollbackFn ): Result { return new Err(error, rollbackFn); } type SafeReturnType = T extends Result ? Result, InferOkType, never> : Result; export function safe(fn: () => Promise): Promise>; export function safe(fn: () => T): SafeReturnType; export function safe( err: ErrorType | (new (...args: any[]) => ErrorType), fn: () => Promise ): Promise>; export function safe( err: ErrorType | (new (...args: any[]) => ErrorType), fn: () => T ): SafeReturnType; export function safe(errOrFn: any, fn?: any) { const hasCustomError = fn !== undefined; const execute = hasCustomError ? fn : errOrFn; function getError(caughtError: Error) { if (!hasCustomError) { return caughtError; } if (typeof errOrFn === "function") { return new errOrFn(caughtError); } return errOrFn; } try { const resultOrPromise = execute(); if (resultOrPromise instanceof Promise) { return resultOrPromise .then((okValue) => { return isResult(okValue) ? okValue : Result.ok(okValue); }) .catch((caughtError) => error(getError(caughtError))); } return isResult(resultOrPromise) ? resultOrPromise : Result.ok(resultOrPromise); } catch (caughtError) { return error(getError(caughtError as Error)); } } type CombineResult unknown) | (() => Promise))[]> = Result< ExtractErrorTypes>, MapResultTupleToOkTypeTuple>, HasAsyncRollbackFunction extends true ? () => Promise : () => void >; export function combine unknown) | (() => Promise))[]>( ...items: T ): HasAsyncThunk extends true ? Promise> : CombineResult { if (!items.length) { throw new Error("Expected at least 1 argument"); } const values: unknown[] = []; const rollbacks: RollbackFunction[] = []; let error: Err | null = null; function rollback() { const reversedRollbacks = rollbacks.reverse(); const wrappedRollbackFns = reversedRollbacks.map((fn) => Result.wrap(fn)); let error: Err | null = null; return forEachValueThunkOrPromise( wrappedRollbackFns, (result: Result) => { if (result.isFailure()) { error = Result.error(result.error) as any; return false; } return true; }, () => error || ok() ); } return forEachValueThunkOrPromise( items, (result: Result) => { if (result.isFailure()) { error = Result.error(result.error, rollback) as any; return false; } values.push(result.value); rollbacks.push(() => result.rollback()); return true; }, () => error || ok(values, rollback) ); } export function wrap Promise>( fn: Fn ): (...args: Parameters) => Promise, never>>; export function wrap any>( fn: Fn ): (...args: Parameters) => Result, never>; export function wrap(fn: any) { return function wrapped(...args: any) { try { const resultOrPromise = fn(...args); if (resultOrPromise instanceof Promise) { return resultOrPromise.then((okValue) => Result.ok(okValue)).catch((err) => error(err)); } return ok(resultOrPromise); } catch (err) { return error(err); } }; } } abstract class Base implements IResult { constructor(protected readonly rollbackFn?: RollbackFn) {} errorOrNull(): ErrorType | null { if (this.isSuccess()) { return null; } return (this as any).error as ErrorType; } getOrNull(): OkType | null { if (this.isFailure()) { return null; } return (this as any).value as OkType; } toString(): string { throw new Error("Method not implemented."); } inspect(): string { return this.toString(); } fold(onSuccess: (value: OkType) => R, onFailure: (error: ErrorType) => R): R; fold(onSuccess: (value: OkType) => Promise, onFailure: (error: ErrorType) => Promise): Promise; fold(onSuccess: any, onFailure: any) { if (this.isFailure()) { return onFailure(this.error); } return onSuccess((this as any).value as OkType); } getOrDefault(defaultValue: OkType): OkType { if (this.isSuccess()) { return this.value; } return defaultValue; } getOrElse(onFailure: (error: ErrorType) => OkType): OkType; getOrElse(onFailure: (error: ErrorType) => Promise): Promise; getOrElse(onFailure: any) { if (this.isSuccess()) { return isAsyncFn(onFailure) ? Promise.resolve(this.value) : this.value; } return onFailure((this as any).error as ErrorType); } getOrThrow(): OkType { if (this.isFailure()) { throw this.error; } return (this as any).value as OkType; } isSuccess(): this is Ok { throw new Error("Method not implemented."); } isFailure(): this is Err { throw new Error("Method not implemented."); } map( fn: (value: OkType) => Promise ): Promise ? T : Result>>; map( fn: (value: OkType) => T ): JoinErrorTypes ? T : Result>; map(fn: any) { if (this.isFailure()) { return isAsyncFn(fn) ? Promise.resolve(this) : this; } const result = Result.safe(() => fn((this as any).value) as any); return result as any; } rollback(): RollbackFn extends RollbackFunction ? RollbackFn extends () => Promise ? Promise> : Result : void { if (this.rollbackFn) { return this.rollbackFn() as any; } return null as any; } } class Ok extends Base< ErrorType, OkType, RollbackFn > { public readonly value: OkType; constructor(val: OkType, rollbackFn?: RollbackFn) { super(rollbackFn); this.value = val; } isSuccess(): this is Ok { return true; } isFailure(): this is Err { return false; } toString(): string { return `Result.Ok(${this.value})`; } forward(): Result { return Result.ok(this.value); } } class Err extends Base< ErrorType, OkType, RollbackFn > { public readonly error: ErrorType; constructor(err: ErrorType, rollbackFn?: RollbackFn) { super(rollbackFn); this.error = err; } isSuccess(): this is Ok { return false; } isFailure(): this is Err { return true; } toString(): string { return `Result.Error(${this.error})`; } forward(): Result { return Result.error(this.error); } }