/** * Type that represents a failable computation. Is parametrized * over the result type and the error type. */ export interface IFailable { /** * Return an object containing the result of * the computation from which the value or error * can be extracted using .isError check. * Useful as an alternative to .match * * @example * ``` * * const failable: IFailable = ...; * if (failable.result.isError) { * // .error can be accessed inside this block * console.log(failable.result.error); * } else { * // .value can be accessed inside this block * console.log(failable.result.value); * } * ``` */ readonly result: IFailableResult; /** * Transform an {@link IFailable} into an {@link IFailable} * by applying the given function to the result value in * case of success. Has no effect in case this is a failure. * @param f Function that transforms a success value. * * @example * ``` * * const numStr: Failable = ...; * const parsed: Failable = numStr.map(parseInt); // or numStr.map(s => parseInt(s)) * ``` */ map(f: (r: Result) => R2): IFailable; /** * Transform the error value of an {@link IFailable} using the * given function. Has no effect if the {@link IFailable} was * a success. * @param f Function for transforming the error value * * @example * ``` * * const result: Failable = ...; * const withErrorCode: Failable = result.mapError(getErrorCode) * ``` */ mapError(f: (e: Error) => E2): IFailable; /** * Pattern match over this IFailable by supplying * a success and failure functions. Both cases * must return a value of type T * @param cases Match cases * * @example * ``` * * const result: Failable = ...; * const num = result.match({ * success: x => x, * failure: err => { * console.log(err); * return 0; * } * }); // num = x if result was successful, otherwise 0 * ``` */ match(cases: IFailableMatchCase): T; /** * Chain another computation to this IFailable that * takes the result value of this IFailable and returns * a new IFailable (possibly of a different type). * The chained computation must be an IFailable whose error * type is a subset of this IFailable's error type. * If not, you can call .mapError on it to convert it's * error into a type compatible with this IFailable. * * This method allows you to chain arbitrary failable computations * dependent on the results of previous ones in the chain that * "short circuit" in case of the first error. * * @param f Function that takes the success value of this * IFailable and returns another IFailable (possibly of another * type) * * @example * ``` * * const computation1: () => Failable = ...; * const computation2: (x: int) => Failable = ...; * const result: Failable = computation1().flatMap(x => computation2(x)) * ``` */ flatMap( f: (r: Result) => IFailable ): IFailable; } /** * Discriminated union for an {@link IFailable} result. * value or error can be extracted from it using an * `if (r.isError)` check */ export type IFailableResult = { readonly isError: true; readonly error: E; } | { readonly isError: false; readonly value: T; }; /** * Container for a value of type T. Used to distinguish expections * thrown by failable from other exceptions. * This is for internal use only. It's not exported. Don't depend * on its behaviour. * @internal */ class ErrorValue { constructor(public readonly value: T) {} } /** * Type that represents a failure result. This is not * a part of the exported API and isn't actually exported * directly. Depend on {@link IFailable} instead. */ class Failure implements IFailable { public readonly isError: true = true; public readonly result: IFailableResult; constructor(public readonly error: E) { this.result = { isError: true, error: error }; } public map(_: (r: R) => R2): IFailable { // tslint:disable-next-line:no-any return this; } public mapError(func: (e: E) => E2): Failure { return new Failure(func(this.error)); } public flatMap(_: (r: R) => IFailable): IFailable { // tslint:disable-next-line:no-any return this; } public match(cases: IFailableMatchCase): T { return cases.failure(this.error); } } /** * Argument type of .match method on an {@link IFailbale}. * It takes an object containing two callbacks; One for * success and failure case. * The value returned by these callbacks should be the * same type. */ export interface IFailableMatchCase { /** * Callback that is run in case of failure. * It is passed the error value of the result. */ failure(e: E): T; /** * Callback that is called in case of success. * It is passed the success value of the result. */ success(v: R): T; } /** * Type that represents a success result. This is not * a part of the exported API and isn't actually exported * directly. Depend on {@link IFailable} instead. */ class Success implements IFailable { public readonly isError: false = false; public readonly result: IFailableResult; constructor(public readonly value: R) { this.result = { isError: false, value: value }; } public isFailure() { return false; } public map(func: (r: R) => R2): IFailable { return new Success(func(this.value)); } public flatMap(func: (r: R) => IFailable): IFailable { return func(this.value).match>({ success: value => new Success(value), failure: e => new Failure(e) }); } public mapError(_: (r: E) => E2): IFailable { // tslint:disable-next-line:no-any return this; } public match(cases: IFailableMatchCase): T { return cases.success(this.value); } } export type FailablePromise = Promise>; export type FailableAsyncFunctionParams = { success(value: T): Promise>; failure(error: E): Promise>; run(f: IFailable): R; }; export type FailableAsyncArg = ( (arg: FailableAsyncFunctionParams) => Promise> ); /** * Async version of failable that takes a computation that * returns a Promise>. It can be combined with * async/await * * @example * ``` * * const computation1: () => FailablePromise = ...; * const computation2: (x: string) => FailablePromise = ...; * const computation3: (x: number) => Failable = ...; * const computation4 = failableAsync(async ({ run, success, failure }) => { * const str = run(await computation1()); * const num1 = run(await computation2(str)); * * // notice that computation3 returns a non async failable so await isn't required * const num = run(computation3(num1)); * if (num > 10) { * return success(num); * } else { * return failure("Number too small"); * } * }) * ``` */ export function failableAsync( f: FailableAsyncArg ): Promise> { return f({ success(value) { return Promise.resolve(new Success(value)); }, failure(e) { return Promise.resolve(new Failure(e)); }, run(result) { return result.match({ failure: error => { throw new ErrorValue(error); }, success: value => value }); } }) .catch(e => { if (e instanceof ErrorValue) { return Promise.resolve(new Failure(e.value)); } else { return Promise.reject(e); } }); } export type FailableArgParams = { /** * Make IFailable from a T * @param value */ success(value: T): IFailable; failure(error: E): IFailable; run(f: IFailable): R; }; export type FailableArg = ( (arg: FailableArgParams) => IFailable ); /** * Creates a failable comutation from a function. * The supplied function receives an object containing * helper functions to create IFailable values. You * need to give generic arguments T and E to it indicating * the success and failure types. * * @param f Failable computation * * @example * ``` * * const computation1: () => Failable = ...; * const computation2: (x: string) => Failable = ...; * const computation3 = failable(({ run, success, failure }) => { * const str = run(computation1()); * const num = run(computation2(str)); * if (num > 10) { * return success(num); * } else { * return failure("Number too small"); * } * }) * ``` */ export function failable( f: FailableArg ): IFailable { try { return f({ success(value) { return new Success(value); }, failure(e) { return new Failure(e); }, run(result) { return result.match({ failure: error => { throw new ErrorValue(error); }, success: value => value }); } }); } catch (e) { if (e instanceof ErrorValue) { return new Failure(e.value); } else { throw e; } } } /** * Create an error {@link IFailable} value. * @param err Error value */ export function failure(err: E): IFailable { return new Failure(err); } /** * Create a successful {@link IFailable} value * @param value Result value */ export function success(value: T): IFailable { return new Success(value); } /** * Helper type for an async function that * takes Req and returns a {@link FailablePromise}. */ export type AsyncFunction< Req, Res, Err > = (req: Req) => FailablePromise; /** * Take an array of elements and apply a failable computation to * the array, one element at a time, returning an IFailable of items. * @param arr Array of values of type T * @param f Function that takes an item of type T from the given array * and returns an IFailable. * @returns A failable containing an array of U values wrapped inside * an {@link IFailable} */ export function mapMultiple(arr: ReadonlyArray, f: (t: T) => IFailable): IFailable { const result: U[] = []; for (const item of arr) { const fail = f(item); if (fail.result.isError) { // since there's no way to get a value from a failure, // we can just typecast the current failure and return it. // tslint:disable:no-any return fail; } else { result.push(fail.result.value); } } return success(result); } export const mapM = mapMultiple; export function isFailableException(e: T): boolean { return e instanceof Failure; } /** * Object containing static functions for {@link IFailable}. * Anything that isn't an instance method should be added here. */ export const Failable = { of: success, success, failure, mapM: mapMultiple, mapMultiple, isFailableException };