UNPKG

typescript-functional-extensions

Version:

A TypeScript implementation of synchronous and asynchronous Maybe and Result monads

567 lines (562 loc) 18.9 kB
import { ResultAsync } from './resultAsync.js'; import { Unit } from './unit.js'; import { isDefined, isFunction, isPromise, isSome, never, pipeFromArray, } from './utilities.js'; /** * Represents a successful or failed operation */ export class Result { /** * Combines several results (and any error messages) into a single result. * The returned result will be a failure if any of the input results are failures. * * @param results The Results to be combined. * @returns A Result that is a success when all the input results are also successes. */ static combine(results) { const resultEntries = Object.entries(results); const failedResults = resultEntries.filter(([, result]) => result.isFailure); const succeededResults = resultEntries.filter(([, result]) => result.isSuccess); if (failedResults.length === 0) { const values = succeededResults.reduce((resultValues, [key, result]) => { resultValues[key] = result.getValueOrThrow(); return resultValues; }, {}); return Result.success(values); } const errorMessages = failedResults .map(([, result]) => result.getErrorOrThrow()) .join(', '); return Result.failure(errorMessages); } static combineAsync(record) { return ResultAsync.combine(record); } static combineInOrderAsync(record) { return ResultAsync.combineInOrder(record); } /** * Creates a new successful Result with the given value * @param value the result of the successful operation * @returns new successful Result */ static success(value) { return isSome(value) ? new Result({ value, error: undefined, isSuccess: true }) : new Result({ value: Unit.Instance, error: undefined, isSuccess: true, }); } static successIf(conditionOrPredicate, state) { const condition = isFunction(conditionOrPredicate) ? conditionOrPredicate() : conditionOrPredicate; return condition ? Result.success(state.value) : Result.failure(state.error); } /** * Creates a new failed Result * @param error the error of the failed operation * @returns new failed Result */ static failure(error) { return new Result({ value: undefined, error, isSuccess: false, }); } static failureIf(conditionOrPredicate, state) { const condition = isFunction(conditionOrPredicate) ? conditionOrPredicate() : conditionOrPredicate; return condition ? Result.failure(state.error) : Result.success(state.value); } /** * Returns only the values of successful Results. If a selector function * is provided, it will be used to map the values to new ones before they * are returned * @param results * @param projection * @returns */ static choose(results, projection) { if (typeof projection === 'function') { const values = []; for (const r of results) { if (r.isFailure) { continue; } const original = r.getValueOrThrow(); values.push(projection(original)); } return values; } else { const values = []; for (const r of results) { if (r.isFailure) { continue; } const original = r.getValueOrThrow(); values.push(original); } return values; } } /** * Creates a new successful Result with the return value * of the give function (or Unit if no value is returned). * If the function throws, a failed Result will * be returned with an error created by the provided errorHandler * @param actionOrFactory * @param errorHandler */ static try(actionOrFactory, errorHandler) { try { const value = actionOrFactory(); return isDefined(value) ? Result.success(value) : Result.success(Unit.Instance); } catch (error) { return Result.failure(errorHandler(error)); } } /** * True if the result operation succeeded */ get isSuccess() { return isDefined(this.state.value); } /** * True if the result operation failed. */ get isFailure() { return !this.isSuccess; } /** * Yields value if the result operation succeeded. * Hint: Use hasValue() upfront to be sure that result operation succeeded. */ get value() { return this.state.value; } /** * Yields error if the result operation failed. * Hint: Use hasError() upfront to be sure that result operation failed. */ get error() { return this.state.error; } /** * Checks if result operation succeeded. */ hasValue() { return this.isSuccess; } /** * Checks if result operation failed. */ hasError() { return !this.isSuccess; } /** * The internal state of the Result */ state = { value: undefined, error: undefined, }; /** * Creates a new Result instance in a guaranteed valid state * @param {{ value?: TValue, error?: TError, isSuccess: boolean }} state the initial state of the Result * @throws {Error} if the provided initial state is invalid */ constructor(state) { const { value, error, isSuccess } = state; if (isSome(value) && !isSuccess) { throw new Error('Value cannot be defined for failed Result'); } else if (isSome(error) && isSuccess) { throw new Error('Error cannot be defined for successful Result'); } else if (!isSome(value) && !isSome(error)) { throw new Error('Value or Error must be defined'); } this.state.value = value ?? undefined; this.state.error = error ?? undefined; } /** * Gets the Result's inner value * @returns {TValue} the inner value if the result suceeded * @throws {Error} if the result failed */ getValueOrThrow() { if (isDefined(this.state.value)) { return this.state.value; } throw Error('No value'); } /** * Gets the Result's inner value * @param defaultOrValueFactory A value or value factory function * @returns {TValue} The Result's value or a default value if the Result failed */ getValueOrDefault(defaultOrValueFactory) { if (this.isSuccess) { return this.getValueOrThrow(); } if (isFunction(defaultOrValueFactory)) { return defaultOrValueFactory(); } return defaultOrValueFactory; } /** * Gets the Result's inner error * @returns {TError} the inner error if the operation failed * @throws {Error} if the result succeeded */ getErrorOrThrow() { if (isDefined(this.state.error)) { return this.state.error; } throw Error('No error'); } getErrorOrDefault(errorOrErrorFactory) { if (this.isFailure) { return this.getErrorOrThrow(); } if (isFunction(errorOrErrorFactory)) { return errorOrErrorFactory(); } return errorOrErrorFactory; } /** * Checks the value of a given predicate against the Result's inner value, * if the Result already succeeded * @param predicate check against the Result's inner value * @param errorOrErrorFactory either an error value or a function to create an error from the Result's inner value * @returns {Result} succeeded if the predicate is true, failed if not */ ensure(predicate, errorOrErrorFactory) { if (this.isFailure) { return this; } const value = this.getValueOrThrow(); if (predicate(value)) { return this; } return isFunction(errorOrErrorFactory) ? Result.failure(errorOrErrorFactory(value)) : Result.failure(errorOrErrorFactory); } /** * Returns a successful Result with the current value if the projection returns a successful Result * @param projection a function given the current Result's value and returns a new Result * @returns If the Result has failed, it is returned. Otherwise, the projection is executed. * If the projection returns a successful Result, a successful Result with the original value is returned. * If the projection returns a failed Result it is returned. */ check(projection) { return this.bind(projection).map((_) => this.getValueOrThrow()); } /** * Similiar to check, but the projection is only executed if the Result has succeeded and the condition or predicate evaluates to true * @param conditionOrPredicate * @param projection * @returns */ checkIf(conditionOrPredicate, projection) { if (this.isFailure) { return this; } const condition = isFunction(conditionOrPredicate) ? conditionOrPredicate(this.getValueOrThrow()) : conditionOrPredicate; return condition ? this.check(projection) : this; } /** * Executes the given operator functions, creating a custom pipeline * @param operations Result operation functions * @returns */ pipe(...operations) { return pipeFromArray(operations)(this); } /** * Maps the value successful Result to a new value * @param projection a function given the value of the current Result which returns a new value * @returns If the Result has failed, a new one with the same error is returned. * Otherwise a new successful Result is returned with the value of the projection. */ map(projection) { return this.isSuccess ? Result.success(projection(this.getValueOrThrow())) : Result.failure(this.getErrorOrThrow()); } /** * Maps the error of a failed Result to a new error * @param projection a function given the error of the current Result which returns a new error * @returns If the Result has succeeded, a new one with the same value is returned. * Otherwise a new failed Result is returned with the error created by the projection. */ mapError(projection) { return this.isFailure ? Result.failure(projection(this.getErrorOrThrow())) : Result.success(this.getValueOrThrow()); } /** * Converts a failed Result into a successful one * @param projection a function that maps the error of the current Result to a value * @returns A successful Result using the current Result's value if it succeeded and the projection's value if it failed */ mapFailure(projection) { return this.isSuccess ? this : Result.success(projection(this.getErrorOrThrow())); } /** * Maps the value successful Result to a new async value wrapped in a ResultAsync * @param projection a function given the value of the current Result which returns a Promise of some value * @returns */ mapAsync(projection) { return this.isSuccess ? ResultAsync.from(projection(this.getValueOrThrow())) : ResultAsync.failure(this.getErrorOrThrow()); } /** * Maps the error of a failed Result to a new async value wrapped in a ResultAsync * @param projection a function given the error of the current Result which returns a Promise of some value * @returns */ mapFailureAsync(projection) { return this.isSuccess ? ResultAsync.from(this) : ResultAsync.from(projection(this.getErrorOrThrow())); } /** * Maps a successful Result to a new Result * @param projection a function given the value of the current Result which returns a new Result of some value * @returns */ bind(projection) { return this.isSuccess ? projection(this.getValueOrThrow()) : Result.failure(this.getErrorOrThrow()); } /** * Maps a successful Result to a new ResultAsync * @param projection * @returns */ bindAsync(projection) { if (this.isFailure) { return ResultAsync.failure(this.getErrorOrThrow()); } const resultAsyncOrPromise = projection(this.getValueOrThrow()); return isPromise(resultAsyncOrPromise) ? ResultAsync.from(resultAsyncOrPromise) : resultAsyncOrPromise; } /** * Maps a failed Result to a new Result * @deprecated Please use `compensate` instead * @param projection * @returns */ bindFailure(projection) { return this.compensate(projection); } /** * Maps a failed Result to a new Result * @param projection * @returns */ compensate(projection) { return this.isSuccess ? this : projection(this.getErrorOrThrow()); } /** * Maps a failed Result to a new ResultAsync * @deprecated Please use `compensateAsync` instead * @param projection * @returns */ bindFailureAsync(projection) { return this.compensateAsync(projection); } /** * Maps a failed Result to a new ResultAsync * @param projection * @returns */ compensateAsync(projection) { if (this.isSuccess) { return ResultAsync.success(this.getValueOrThrow()); } const resultAsyncOrPromise = projection(this.getErrorOrThrow()); return isPromise(resultAsyncOrPromise) ? ResultAsync.from(resultAsyncOrPromise) : resultAsyncOrPromise; } /** * Executes an action if the current Result has succeeded * @param action a function given the value of the current Result * @returns the current Result */ tap(action) { if (this.isSuccess) { action(this.getValueOrThrow()); } return this; } /** * Executes an action if the current Result has failed * @param action a function given the error of the current Result * @returns the current Result */ tapFailure(action) { if (this.isFailure) { action(this.getErrorOrThrow()); } return this; } /** * Executes an async action if the Result succeeded * @param action a function given the Result's value returns a Promise * @returns a ResultAsync */ tapAsync(action) { if (this.isFailure) { return ResultAsync.failure(this.getErrorOrThrow()); } const value = this.getValueOrThrow(); return ResultAsync.from(action(value).then(() => value)); } /** * Executes and action if the given condition evaluates to true and the Result has succeeded * @param conditionOrPredicate a boolean value or predicate * @param action a function given the Result's value * @returns the current Result */ tapIf(conditionOrPredicate, action) { if (this.isFailure) { return this; } const value = this.getValueOrThrow(); if (isFunction(conditionOrPredicate)) { conditionOrPredicate(value) && action(value); } else { conditionOrPredicate && action(value); } return this; } /** * Executes the action on both success and failure. * @param action a function with no parameters returning no value * @returns the current Result */ tapEither(action) { action(); return this; } /** * Executes the asynchronous action on both success and failure. * @param action a function * @returns the current Result wrapped in a ResultAsync */ tapEitherAsync(action) { return ResultAsync.from(action().then(() => this)); } /** * Executes functions for a Result in either the successful and failed state * @param matcher * @returns */ match(matcher) { if (this.isSuccess) { const successValue = matcher.success(this.getValueOrThrow()); return isDefined(successValue) ? successValue : Unit.Instance; } if (this.isFailure) { const failureValue = matcher.failure(this.getErrorOrThrow()); return isDefined(failureValue) ? failureValue : Unit.Instance; } return never(); } /** * Executes the same function for both failed and successful Results * @param projection * @returns */ finally(projection) { return projection(this); } /** * * @param action * @param errorHandler * @returns */ onSuccessTry(action, errorHandler) { if (this.isFailure) { return this; } const value = this.getValueOrThrow(); try { action(value); return Result.success(value); } catch (error) { return Result.failure(errorHandler(error)); } } /** * * @param asyncAction * @param errorHander * @returns */ onSuccessTryAsync(asyncAction, errorHander) { if (this.isFailure) { return ResultAsync.failure(this.getErrorOrThrow()); } const promiseFactory = async () => { const value = this.getValueOrThrow(); try { await asyncAction(value); return Result.success(value); } catch (error) { return Result.failure(errorHander(error)); } }; return ResultAsync.from(promiseFactory()); } /** * Returns a string representation of the Result state (success/failure) * @returns */ toString() { return this.isSuccess ? 'Result.success' : 'Result.failure'; } debug() { return this.isFailure ? `{ Result error: [${this.getErrorOrThrow()}] }` : `{ Result value: [${this.getValueOrThrow()}] }`; } equals(result) { return ((this.isSuccess && result.isSuccess && this.getValueOrThrow() === result.getValueOrThrow()) || (this.isFailure && result.isFailure && this.getErrorOrThrow() === result.getErrorOrThrow())); } }