typescript-functional-extensions
Version:
A TypeScript implementation of synchronous and asynchronous Maybe and Result monads
571 lines (566 loc) • 19.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Result = void 0;
const resultAsync_js_1 = require("./resultAsync.js");
const unit_js_1 = require("./unit.js");
const utilities_js_1 = require("./utilities.js");
/**
* Represents a successful or failed operation
*/
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_js_1.ResultAsync.combine(record);
}
static combineInOrderAsync(record) {
return resultAsync_js_1.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 (0, utilities_js_1.isSome)(value)
? new Result({ value, error: undefined, isSuccess: true })
: new Result({
value: unit_js_1.Unit.Instance,
error: undefined,
isSuccess: true,
});
}
static successIf(conditionOrPredicate, state) {
const condition = (0, utilities_js_1.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 = (0, utilities_js_1.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 (0, utilities_js_1.isDefined)(value)
? Result.success(value)
: Result.success(unit_js_1.Unit.Instance);
}
catch (error) {
return Result.failure(errorHandler(error));
}
}
/**
* True if the result operation succeeded
*/
get isSuccess() {
return (0, utilities_js_1.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 ((0, utilities_js_1.isSome)(value) && !isSuccess) {
throw new Error('Value cannot be defined for failed Result');
}
else if ((0, utilities_js_1.isSome)(error) && isSuccess) {
throw new Error('Error cannot be defined for successful Result');
}
else if (!(0, utilities_js_1.isSome)(value) && !(0, utilities_js_1.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 ((0, utilities_js_1.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 ((0, utilities_js_1.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 ((0, utilities_js_1.isDefined)(this.state.error)) {
return this.state.error;
}
throw Error('No error');
}
getErrorOrDefault(errorOrErrorFactory) {
if (this.isFailure) {
return this.getErrorOrThrow();
}
if ((0, utilities_js_1.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 (0, utilities_js_1.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 = (0, utilities_js_1.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 (0, utilities_js_1.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_js_1.ResultAsync.from(projection(this.getValueOrThrow()))
: resultAsync_js_1.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_js_1.ResultAsync.from(this)
: resultAsync_js_1.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_js_1.ResultAsync.failure(this.getErrorOrThrow());
}
const resultAsyncOrPromise = projection(this.getValueOrThrow());
return (0, utilities_js_1.isPromise)(resultAsyncOrPromise)
? resultAsync_js_1.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_js_1.ResultAsync.success(this.getValueOrThrow());
}
const resultAsyncOrPromise = projection(this.getErrorOrThrow());
return (0, utilities_js_1.isPromise)(resultAsyncOrPromise)
? resultAsync_js_1.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_js_1.ResultAsync.failure(this.getErrorOrThrow());
}
const value = this.getValueOrThrow();
return resultAsync_js_1.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 ((0, utilities_js_1.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_js_1.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 (0, utilities_js_1.isDefined)(successValue) ? successValue : unit_js_1.Unit.Instance;
}
if (this.isFailure) {
const failureValue = matcher.failure(this.getErrorOrThrow());
return (0, utilities_js_1.isDefined)(failureValue) ? failureValue : unit_js_1.Unit.Instance;
}
return (0, utilities_js_1.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_js_1.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_js_1.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()));
}
}
exports.Result = Result;