typescript-monads
Version:
Write cleaner TypeScript
1,372 lines (1,325 loc) • 68.2 kB
text/typescript
import type { IMaybe } from '../maybe/maybe.interface'
import type { IResultMatchPattern, IResult } from './result.interface'
import { maybe, none } from '../maybe/maybe.factory'
export abstract class Result<TOk, TFail> implements IResult<TOk, TFail> {
public static ok<TOk, TFail>(value: TOk): IResult<TOk, TFail> {
return new OkResult<TOk, TFail>(value)
}
public static fail<TOk, TFail>(value: TFail): IResult<TOk, TFail> {
return new FailResult<TOk, TFail>(value)
}
/**
* Creates a Result from a Promise.
*
* Creates a Promise that will resolve to a Result that is:
* - Ok containing the resolved value if the promise resolves successfully
* - Fail containing the rejection reason if the promise rejects
*
* This static method makes it easy to convert Promise-based APIs to Result-based
* error handling, giving you more explicit control flow and type safety.
*
* @param promise The promise to convert to a Result
* @returns A Promise that resolves to a Result containing either the value or error
*
* @example
* // Convert a promise to a Result
* const resultPromise = Result.fromPromise(fetchData());
*
* resultPromise.then(result => {
* result.match({
* ok: data => renderData(data),
* fail: error => showError(error)
* });
* });
*
* // With Promise chaining
* Result.fromPromise(fetchData())
* .then(result => {
* if (result.isOk()) {
* const data = result.unwrap();
* return renderData(data);
* } else {
* const error = result.unwrapFail();
* return showError(error);
* }
* });
*/
public static fromPromise<TOk, TFail = unknown>(promise: Promise<TOk>): Promise<IResult<TOk, TFail>> {
return promise
.then((value: TOk) => new OkResult<TOk, TFail>(value))
.catch((error: TFail) => new FailResult<TOk, TFail>(error))
}
/**
* Creates a Result from an Observable.
*
* Creates a Promise that will resolve to a Result that is:
* - Ok containing the first emitted value if the observable emits a value
* - Fail containing the provided error if the observable completes without emitting
* - Fail containing the error if the observable errors
*
* This static method bridges the reactive Observable world with the Result monad,
* providing a way to handle emissions, completion, and errors in a functional style.
*
* Note that this transformation changes the timing model from continuous/reactive
* to a one-time asynchronous result. Only the first emission is captured, and the
* observable is no longer reactive after transformation.
*
* @param observable The observable to convert to a Result
* @param defaultError The error to use if the observable completes without emitting
* @returns A Promise that resolves to a Result containing either the value or error
*
* @requires rxjs@^7.0
* @example
* // Convert an observable to a Result
* Result.fromObservable(
* userService.getUser(userId),
* new Error('User not found')
* ).then(result => {
* result.match({
* ok: user => renderUser(user),
* fail: error => showUserNotFound(error)
* });
* });
*/
public static fromObservable<TOk, TFail>(
observable: import('rxjs').Observable<TOk>,
defaultError: TFail
): Promise<IResult<TOk, TFail>> {
return import('rxjs').then(({ firstValueFrom, take, map, catchError }) => {
return firstValueFrom(
observable.pipe(
take(1),
map((value: TOk) => new OkResult<TOk, TFail>(value)),
catchError((error: unknown) => {
// Return the error from the observable directly rather than using EMPTY
return [new FailResult<TOk, TFail>(error as TFail)]
})
)
).then(
(result: IResult<TOk, TFail>) => result,
() => new FailResult<TOk, TFail>(defaultError) // Handle the case where firstValueFrom rejects
)
})
}
/**
* Transforms an array of Result values into a Result containing an array of values.
*
* If all Results in the input array are Ok, returns an Ok Result containing an array of all values.
* If any Result in the input array is Fail, returns a Fail Result containing the first error encountered.
*
* This is similar to how Promise.all works but for Results, allowing you to combine
* multiple Results and propagate errors if any occur.
*
* @param results An array of Result values
* @returns A Result containing either an array of all Ok values or the first Fail error
*
* @example
* // All Results are Ok
* const result1 = Result.sequence([
* ok(1),
* ok(2),
* ok(3)
* ]);
* // result1 is Ok([1, 2, 3])
*
* // One Result is Fail
* const result2 = Result.sequence([
* ok(1),
* fail(new Error('Failed')),
* ok(3)
* ]);
* // result2 is Fail(Error('Failed'))
*/
public static sequence<T, E>(results: ReadonlyArray<IResult<T, E>>): IResult<ReadonlyArray<T>, E> {
if (results.length === 0) {
return new OkResult<ReadonlyArray<T>, E>([])
}
const values: T[] = []
for (const r of results) {
if (r.isFail()) {
return new FailResult<ReadonlyArray<T>, E>(r.unwrapFail())
}
values.push(r.unwrap())
}
return new OkResult<ReadonlyArray<T>, E>(values)
}
/**
* Alias for sequence, transforms an array of Result values into a Result containing an array of values.
*
* If all Results in the input array are Ok, returns an Ok Result containing an array of all values.
* If any Result in the input array is Fail, returns a Fail Result containing the first error encountered.
*
* Named to align with Promise.all for familiarity.
*
* @param results An array of Result values
* @returns A Result containing either an array of all Ok values or the first Fail error
*
* @example
* // All Results are Ok
* const result1 = Result.all([
* ok(1),
* ok(2),
* ok(3)
* ]);
* // result1 is Ok([1, 2, 3])
*
* // One Result is Fail
* const result2 = Result.all([
* ok(1),
* fail(new Error('Failed')),
* ok(3)
* ]);
* // result2 is Fail(Error('Failed'))
*/
public static all<T, E>(results: ReadonlyArray<IResult<T, E>>): IResult<ReadonlyArray<T>, E> {
return Result.sequence(results)
}
/**
* Type guard that determines if this Result is an Ok variant.
*
* This method acts as a TypeScript type guard, narrowing the type of the Result
* to OkResult when it returns true, which helps with type safety in conditional blocks.
*
* @returns true if this Result is an Ok variant, false otherwise
*
* @example
* const result = getUser(userId);
*
* if (result.isOk()) {
* // TypeScript knows that result is OkResult<User, Error> here
* const user = result.unwrap(); // Safe to call
* console.log(user.name);
* } else {
* // TypeScript knows that result is FailResult<User, Error> here
* const error = result.unwrapFail(); // Safe to call
* console.error(error.message);
* }
*/
abstract isOk(): this is OkResult<TOk, TFail>
/**
* Type guard that determines if this Result is a Fail variant.
*
* This method acts as a TypeScript type guard, narrowing the type of the Result
* to FailResult when it returns true, which helps with type safety in conditional blocks.
*
* @returns true if this Result is a Fail variant, false otherwise
*
* @example
* const result = authenticate(credentials);
*
* if (result.isFail()) {
* // TypeScript knows that result is FailResult<Session, AuthError> here
* const error = result.unwrapFail(); // Safe to call
* handleAuthError(error);
* } else {
* // TypeScript knows that result is OkResult<Session, AuthError> here
* startSession(result.unwrap()); // Safe to call
* }
*/
abstract isFail(): this is FailResult<TOk, TFail>
/**
* Extracts the Ok value from this Result.
*
* This method should only be called when you're certain that the Result is an Ok variant.
* If the Result is a Fail variant, this method will throw an exception.
*
* @returns The contained Ok value
* @throws Error if the Result is a Fail variant
*
* @example
* // Safe usage with type guard
* const result = parseJson<Config>(jsonString);
*
* if (result.isOk()) {
* const config = result.unwrap();
* initializeApp(config);
* }
*
* // Alternative safe usage with pattern matching
* result.match({
* ok: config => initializeApp(config),
* fail: error => showError(error)
* });
*
* // Unsafe usage (might throw)
* const config = result.unwrap(); // Throws if result is Fail
*/
abstract unwrap(): TOk | never
/**
* Extracts the Ok value from this Result or returns a default value.
*
* This method provides a safe way to unwrap a Result without risking exceptions.
* If the Result is an Ok variant, the contained value is returned.
* If the Result is a Fail variant, the provided default value is returned.
*
* @param opt The default value to return if this Result is a Fail variant
* @returns The contained Ok value or the provided default
*
* @example
* // Using unwrapOr for default values
* const userResult = getUserById(userId);
*
* // If user not found, use a guest user
* const user = userResult.unwrapOr({
* id: 0,
* name: "Guest",
* role: "visitor"
* });
*
* // Using unwrapOr in a chain
* const userName = getUserById(userId)
* .map(user => user.name)
* .unwrapOr("Unknown User");
*/
abstract unwrapOr(opt: TOk): TOk
/**
* Extracts the Fail value from this Result.
*
* This method should only be called when you're certain that the Result is a Fail variant.
* If the Result is an Ok variant, this method will throw an exception.
*
* @returns The contained Fail value
* @throws ReferenceError if the Result is an Ok variant
*
* @example
* // Safe usage with type guard
* const result = validateInput(formData);
*
* if (result.isFail()) {
* const validationErrors = result.unwrapFail();
* displayErrors(validationErrors);
* }
*
* // Alternative safe usage with pattern matching
* result.match({
* ok: data => submitForm(data),
* fail: errors => displayErrors(errors)
* });
*/
abstract unwrapFail(): TFail | never
/**
* Converts this Result's Ok value to a Maybe.
*
* This method transforms a Result into a Maybe, focusing on the success path:
* - If this Result is an Ok variant, returns a Some Maybe containing the value
* - If this Result is a Fail variant, returns a None Maybe
*
* This is useful when you want to continue with Maybe operations and no longer
* need to track the specific error type.
*
* @returns A Maybe containing the Ok value if present, or None
*
* @example
* // Converting from Result to Maybe
* const userResult: Result<User, ApiError> = fetchUser(userId);
*
* // Focus only on the success case by converting to Maybe
* const userMaybe: Maybe<User> = userResult.maybeOk();
*
* // Continue with Maybe operations
* const userName = userMaybe
* .map(user => user.name)
* .valueOr("Unknown User");
*/
abstract maybeOk(): IMaybe<NonNullable<TOk>>
/**
* Converts this Result's Fail value to a Maybe.
*
* This method transforms a Result into a Maybe, focusing on the failure path:
* - If this Result is a Fail variant, returns a Some Maybe containing the error
* - If this Result is an Ok variant, returns a None Maybe
*
* This is useful when you want to specifically work with errors when they occur,
* but don't care about the success value.
*
* @returns A Maybe containing the Fail value if present, or None
*
* @example
* // Collecting errors across multiple operations
* const results: Array<Result<any, Error>> = runValidations();
*
* const errors = results
* .map(result => result.maybeFail())
* .filter(maybeErr => maybeErr.isSome())
* .map(maybeErr => maybeErr.valueOrThrow());
*
* if (errors.length > 0) {
* displayErrors(errors);
* }
*/
abstract maybeFail(): IMaybe<TFail>
/**
* Applies a pattern matching object to this Result.
*
* This method provides a functional way to handle both Ok and Fail variants
* in a single expression, enforcing exhaustive handling of all cases.
*
* @typeParam M - The return type of the pattern matching functions
* @param fn - An object containing functions to handle each variant:
* - `ok`: Function that processes the Ok value
* - `fail`: Function that processes the Fail value
* @returns The result of applying the matching function to the contained value
*
* @example
* // Basic pattern matching with different return types
* const result = fetchData();
*
* const message = result.match({
* ok: data => `Successfully loaded ${data.items.length} items`,
* fail: error => `Error: ${error.message}`
* });
*
* // Pattern matching for control flow
* result.match({
* ok: data => {
* renderData(data);
* updateLastFetchTime();
* },
* fail: error => {
* logError(error);
* showRetryButton();
* }
* });
*
* // Pattern matching with transformations
* const apiResponse = makeApiCall()
* .match({
* ok: successResponse => ({
* status: 'success',
* data: successResponse
* }),
* fail: errorResponse => ({
* status: 'error',
* message: errorResponse.message,
* code: errorResponse.code
* })
* });
*/
abstract match<M>(fn: IResultMatchPattern<TOk, TFail, M>): M
/**
* Maps the Ok value of this Result using the provided function.
*
* If this Result is an Ok variant, this method transforms the contained value using
* the provided function and returns a new Ok Result with the transformed value.
* If this Result is a Fail variant, it returns a new Fail Result with the same error.
*
* This operation is similar to Array.map but for a Result's Ok value.
*
* @typeParam M - The type of the mapped value
* @param fn - A function that transforms the Ok value
* @returns A new Result with the transformed value if Ok, or the original error if Fail
*
* @example
* // Transforming user data
* const userResult = fetchUser(userId);
*
* const userProfileResult = userResult.map(user => ({
* name: user.name,
* email: user.email,
* avatar: user.avatarUrl || 'default-avatar.png'
* }));
*
* // Chaining multiple transformations
* const userNameResult = fetchUser(userId)
* .map(user => user.profile)
* .map(profile => profile.name)
* .map(name => name.toUpperCase());
*
* // Error case is automatically propagated
* const result = validate("invalid-input") // Returns Fail
* .map(value => process(value)) // Map is not applied
* .map(result => format(result)); // Map is not applied
*
* // result is still the original Fail value
*/
abstract map<M>(fn: (val: TOk) => M): IResult<M, TFail>
/**
* Maps the Fail value of this Result using the provided function.
*
* If this Result is a Fail variant, this method transforms the error using
* the provided function and returns a new Fail Result with the transformed error.
* If this Result is an Ok variant, it returns a new Ok Result with the same value.
*
* This is useful for transforming errors while preserving their failure state.
*
* @typeParam M - The type of the mapped error
* @param fn - A function that transforms the Fail value
* @returns A new Result with the transformed error if Fail, or the original value if Ok
*
* @example
* // Enriching error information
* const result = fetchData()
* .mapFail(error => ({
* ...error,
* timestamp: new Date(),
* context: 'fetchData'
* }));
*
* // Converting between error types
* const standardizedResult = externalApiCall()
* .mapFail(apiError => new AppError({
* code: mapErrorCode(apiError.code),
* message: apiError.message,
* source: 'ExternalAPI'
* }));
*
* // Localizing error messages
* const localizedResult = validateInput(form)
* .mapFail(errors => errors.map(err => ({
* ...err,
* message: translateErrorMessage(err.message, currentLocale)
* })));
*/
abstract mapFail<M>(fn: (err: TFail) => M): IResult<TOk, M>
/**
* Chains a function that returns another Result.
*
* If this Result is an Ok variant, this method applies the function to the contained value,
* which returns a new Result. This allows for sequencing operations that might fail.
* If this Result is a Fail variant, it returns a new Fail Result with the same error without
* calling the function.
*
* This operation is similar to flatMap/bind in other functional programming contexts.
*
* @typeParam M - The type of the value in the returned Result
* @param fn - A function that takes the Ok value and returns a new Result
* @returns The Result returned by fn if this Result is Ok, or a Fail Result with the original error
*
* @example
* // Sequential operations that might fail
* const result = parseConfigFile(filePath)
* .flatMap(config => validateConfig(config))
* .flatMap(validConfig => initializeSystem(validConfig));
*
* // Database transaction example
* const transactionResult = connectToDatabase()
* .flatMap(connection => beginTransaction(connection))
* .flatMap(transaction => executeQueries(transaction))
* .flatMap(transaction => commitTransaction(transaction));
*
* // Early return on failure
* const userResult = authenticateUser(credentials) // Might return Fail
* .flatMap(user => authorizeUser(user, resource)) // Only called if authentication succeeds
* .flatMap(user => loadUserProfile(user)); // Only called if authorization succeeds
*/
abstract flatMap<M>(fn: (val: TOk) => IResult<M, TFail>): IResult<M, TFail>
/**
* Maps the success value of this Result to a Maybe, and then flattens the resulting structure.
*
* This method is particularly useful when working with optional properties or values that might be undefined/null.
* It allows for seamless chaining of Result and Maybe monads without explicit unwrapping and re-wrapping.
*
* @typeParam M - The type of the value contained in the returned Result if successful
* @param fn - A function that takes the success value of this Result and returns a Maybe
* @param err - The error value to use if the Maybe is None
* @returns
* - If this Result is a Fail: A Fail Result containing the original error
* - If this Result is an Ok and fn returns Some: An Ok Result containing the unwrapped value
* - If this Result is an Ok but fn returns None: A Fail Result containing the provided err
*
* @example
* // Type definitions
* interface User {
* id: number;
* profile?: {
* name: string;
* email: string;
* };
* }
*
* // Success path with Some
* const getUser = (): Result<User, Error> =>
* ok({ id: 1, profile: { name: "Alice", email: "alice@example.com" } });
*
* // Chain to access a potentially undefined property safely
* const getName = getUser()
* .flatMapMaybe(
* user => maybe(user.profile),
* new Error("Profile not found")
* )
* .map(profile => profile.name);
*
* // getName is Result<string, Error> containing "Alice"
*
* // Using with nullish values
* type Response = { data?: { value: number } | null };
*
* const response: Response = { data: null };
* const value = ok<Response, string>(response)
* .flatMapMaybe(
* res => maybe(res.data),
* "No data available"
* )
* .flatMapMaybe(
* data => maybe(data.value),
* "Value not present"
* );
*
* // Working with arrays and optional chaining
* interface Post { comments?: Array<{ id: number, text: string }> }
*
* const getFirstComment = (post: Post): Result<string, string> =>
* ok(post)
* .flatMapMaybe(
* p => maybe(p.comments),
* "No comments found"
* )
* .flatMapMaybe(
* comments => maybe(comments[0]),
* "Comment list is empty"
* )
* .map(comment => comment.text);
*/
abstract flatMapMaybe<M>(fn: (val: TOk) => IMaybe<M>, err: TFail): IResult<M, TFail>
/**
* Converts an Ok Result into a Fail Result using a transformation function.
*
* This method inverts the Result's state by:
* - If this Result is an Ok variant, it applies the function to the contained value
* to generate an error and returns a Fail Result with that error
* - If this Result is a Fail variant, it returns the original Fail Result unchanged
*
* This is useful for scenarios where success conditions need to be converted to failures
* based on the content of the success value.
*
* @param fn - A function that transforms the Ok value into a Fail value
* @returns A Fail Result with the generated error, or the original Fail Result
*
* @example
* // Implementing validation logic
* const userResult = getUserById(userId);
*
* const validatedUser = userResult.toFailWhenOk(user => {
* if (!user.isActive) return new Error("User account is inactive");
* if (user.accessLevel < requiredLevel) return new Error("Insufficient access level");
* return null; // This case won't happen due to TypeScript's return type checking
* });
*
* // Implementing business rule validation
* const orderResult = createOrder(orderData);
*
* const validatedOrder = orderResult.toFailWhenOk(order => {
* if (order.items.length === 0) return new ValidationError("Order must contain at least one item");
* if (order.total < minimumOrderAmount) return new ValidationError(`Order total must be at least ${minimumOrderAmount}`);
* return new ValidationError(""); // TypeScript requires a return, but this won't be reached in valid code
* });
*/
abstract toFailWhenOk(fn: (val: TOk) => TFail): IResult<TOk, TFail>
/**
* Converts an Ok Result into a Fail Result using a provided error value.
*
* This method inverts the Result's state by:
* - If this Result is an Ok variant, it returns a Fail Result with the provided error
* - If this Result is a Fail variant, it returns a Fail Result with the provided error,
* replacing the original error
*
* This is useful for scenarios where you want to override or standardize error values.
*
* @param val - The error value to use in the returned Fail Result
* @returns A Fail Result containing the provided error
*
* @example
* // Standardizing error messages
* const result = parseInput(rawInput)
* .toFailWhenOkFrom(new Error("Input validation failed"));
*
* // Conditional error replacement
* let result = processData(data);
*
* if (shouldUseStandardError) {
* result = result.toFailWhenOkFrom(standardError);
* }
*
* // Overriding authentication errors with a generic message
* const authResult = authenticate(credentials)
* .toFailWhenOkFrom(new Error("Authentication failed"));
*/
abstract toFailWhenOkFrom(val: TFail): IResult<TOk, TFail>
/**
* Executes side-effect functions based on the Result variant without changing the Result.
*
* This method allows you to perform actions that don't affect the Result's value:
* - If this Result is an Ok variant, it calls the provided `ok` function with the contained value
* - If this Result is a Fail variant, it calls the provided `fail` function with the error
*
* Both functions are optional; if not provided, nothing happens for that variant.
*
* @param val - An object containing optional functions for Ok and Fail variants
*
* @example
* // Logging based on Result variant
* fetchData().tap({
* ok: data => console.log("Data fetched successfully:", data),
* fail: error => console.error("Failed to fetch data:", error)
* });
*
* // Metrics and analytics
* processPayment(paymentInfo).tap({
* ok: result => {
* analytics.trackEvent("payment_success", {
* amount: result.amount,
* method: result.method
* });
* },
* fail: error => {
* analytics.trackEvent("payment_failure", {
* error: error.code,
* message: error.message
* });
* }
* });
*
* // Partial application (only handling one variant)
* validateInput(formData).tap({
* fail: errors => highlightFormErrors(errors)
* });
*/
abstract tap(val: Partial<IResultMatchPattern<TOk, TFail, void>>): void
/**
* Executes a side-effect function when this Result is an Ok variant.
*
* This method is a specialized version of `tap` that only handles the Ok case:
* - If this Result is an Ok variant, it calls the provided function with the contained value
* - If this Result is a Fail variant, it does nothing
*
* @param f - A function to execute with the Ok value
*
* @example
* // Logging successful operations
* saveUser(userData).tapOk(user => {
* console.log(`User ${user.id} saved successfully`);
* updateLastSavedTimestamp();
* });
*
* // UI updates on success
* fetchData().tapOk(data => {
* updateUI(data);
* hideLoadingIndicator();
* });
*
* // Analytics for successful operations
* checkout(cart).tapOk(order => {
* analytics.trackPurchase({
* orderId: order.id,
* amount: order.total,
* items: order.items.length
* });
* });
*/
abstract tapOk(f: (val: TOk) => void): void
/**
* Executes a side-effect function when this Result is a Fail variant.
*
* This method is a specialized version of `tap` that only handles the Fail case:
* - If this Result is a Fail variant, it calls the provided function with the error
* - If this Result is an Ok variant, it does nothing
*
* @param f - A function to execute with the Fail value
*
* @example
* // Error logging
* processRequest(req).tapFail(error => {
* logger.error("Request processing failed", {
* error: error.message,
* stack: error.stack,
* requestId: req.id
* });
* });
*
* // UI error handling
* submitForm(formData).tapFail(errors => {
* displayErrors(errors);
* highlightInvalidFields(errors);
* scrollToFirstError();
* });
*
* // Monitoring and alerting
* criticalOperation().tapFail(error => {
* if (error.severity === 'high') {
* alertOps(error);
* incrementFailureCounter();
* }
* });
*/
abstract tapFail(f: (val: TFail) => void): void
/**
* Executes side-effect functions and returns the original Result for chaining.
*
* This method is similar to `tap`, but returns the Result itself to allow for
* further method chaining:
* - If this Result is an Ok variant, it calls the provided `ok` function with the contained value
* - If this Result is a Fail variant, it calls the provided `fail` function with the error
*
* Both functions are optional; if not provided, nothing happens for that variant.
*
* @param val - An object containing optional functions for Ok and Fail variants
* @returns This Result, unchanged
*
* @example
* // Chaining operations with logging
* return fetchUser(userId)
* .tapThru({
* ok: user => console.log(`User ${user.id} fetched`),
* fail: err => console.error(`Failed to fetch user: ${err.message}`)
* })
* .map(user => transformUser(user))
* .tapThru({
* ok: profile => console.log(`User profile created`)
* });
*
* // Progressive UI updates in a chain
* processForm(data)
* .tapThru({
* ok: () => updateProgressBar(0.33)
* })
* .flatMap(validated => saveToDatabase(validated))
* .tapThru({
* ok: () => updateProgressBar(0.66),
* fail: err => showErrorNotification(err)
* })
* .flatMap(saved => notifyUser(saved))
* .tapThru({
* ok: () => updateProgressBar(1.0),
* fail: err => showErrorNotification(err)
* });
*/
abstract tapThru(val: Partial<IResultMatchPattern<TOk, TFail, void>>): IResult<TOk, TFail>
/**
* Executes a side-effect function when this Result is an Ok variant and returns the original
* Result for chaining.
*
* This method is a specialized version of `tapThru` that only handles the Ok case:
* - If this Result is an Ok variant, it calls the provided function with the contained value
* - If this Result is a Fail variant, it does nothing
*
* In both cases, it returns the original Result unchanged.
*
* @param fn - A function to execute with the Ok value
* @returns This Result, unchanged
*
* @example
* // Chaining with logging for successful operations
* return getUserById(userId)
* .tapOkThru(user => console.log(`Found user: ${user.name}`))
* .map(user => user.profile)
* .tapOkThru(profile => console.log(`Profile accessed`))
* .flatMap(profile => getProfileSettings(profile.id));
*
* // Progressive UI updates on success
* submitOrder(order)
* .tapOkThru(() => {
* showMessage("Order submitted");
* updateProgressStep(1);
* })
* .flatMap(order => processPayment(order))
* .tapOkThru(() => {
* showMessage("Payment processed");
* updateProgressStep(2);
* })
* .flatMap(order => finalizeOrder(order));
*/
abstract tapOkThru(fn: (val: TOk) => void): IResult<TOk, TFail>
/**
* Executes a side-effect function when this Result is a Fail variant and returns the original
* Result for chaining.
*
* This method is a specialized version of `tapThru` that only handles the Fail case:
* - If this Result is a Fail variant, it calls the provided function with the error
* - If this Result is an Ok variant, it does nothing
*
* In both cases, it returns the original Result unchanged.
*
* @param fn - A function to execute with the Fail value
* @returns This Result, unchanged
*
* @example
* // Chaining with error logging
* return validateInput(input)
* .tapFailThru(errors => logValidationErrors(errors))
* .flatMap(input => processInput(input))
* .tapFailThru(error => logProcessingError(error))
* .flatMap(result => saveResult(result));
*
* // Progressive error handling
* authenticateUser(credentials)
* .tapFailThru(error => {
* logAuthFailure(error);
* updateLoginAttempts();
* })
* .flatMap(user => authorizeUser(user, resource))
* .tapFailThru(error => {
* logAuthorizationFailure(error);
* recordAccessAttempt(resource);
* });
*
* // Analytics tracking in a chain
* checkout(cart)
* .tapFailThru(error => {
* analytics.trackEvent("checkout_failure", {
* error: error.code,
* step: "initial_validation"
* });
* })
* .flatMap(validCart => processPayment(validCart))
* .tapFailThru(error => {
* analytics.trackEvent("checkout_failure", {
* error: error.code,
* step: "payment_processing"
* });
* });
*/
abstract tapFailThru(fn: (val: TFail) => void): IResult<TOk, TFail>
/**
* Transforms a Fail Result into an Ok Result using a recovery function.
*
* This method provides error handling by:
* - If this Result is a Fail variant, it applies the function to the error to generate a recovery value
* and returns a new Ok Result with that value
* - If this Result is an Ok variant, it returns the original Result unchanged
*
* This is similar to a catch block in try/catch, but in a functional style.
*
* @param fn - A function that transforms the Fail value into a recovery value
* @returns An Ok Result with either the original value or the recovered value
*
* @example
* // Providing default values
* const userResult = getUserById(userId)
* .recover(error => ({
* id: 0,
* name: "Guest User",
* isGuest: true
* }));
*
* // userResult is guaranteed to be Ok
* const user = userResult.unwrap(); // Safe, will never throw
*
* // Error logging with recovery
* fetchData()
* .tapFail(error => logError(error))
* .recover(error => {
* const fallbackData = getLocalData();
* trackRecovery("data_fetch", error);
* return fallbackData;
* })
* .map(data => processData(data)); // This will always run with either fetched or fallback data
*/
abstract recover(fn: (err: TFail) => TOk): IResult<TOk, TFail>
/**
* Transforms a Fail Result by applying a function that returns another Result.
*
* This method provides advanced error recovery by:
* - If this Result is a Fail variant, it applies the function to the error, which returns a new Result
* - If this Result is an Ok variant, it returns the original Result unchanged
*
* This is useful for fallback operations that might themselves fail.
*
* @param fn - A function that takes the Fail value and returns a new Result
* @returns The original Result if Ok, or the Result returned by the function if Fail
*
* @example
* // Trying a fallback operation that might also fail
* fetchFromPrimaryAPI()
* .recoverWith(error => {
* logFailure(error, "primary_api");
* // Try the backup API, which might also fail
* return fetchFromBackupAPI();
* })
* .match({
* ok: data => renderData(data),
* fail: error => showFatalError("All data sources failed")
* });
*
* // Authentication with multiple strategies
* authenticateWithPassword(credentials)
* .recoverWith(error => {
* if (error.code === "CREDENTIALS_EXPIRED") {
* return authenticateWithToken(refreshToken);
* }
* return fail(error); // Pass through other errors
* })
* .recoverWith(error => {
* if (error.code === "TOKEN_EXPIRED") {
* return authenticateWithOAuth();
* }
* return fail(error); // Pass through other errors
* });
*/
abstract recoverWith(fn: (err: TFail) => IResult<TOk, TFail>): IResult<TOk, TFail>
/**
* Returns this Result if it's Ok, otherwise returns the provided fallback Result.
*
* This method allows for specifying an alternative Result:
* - If this Result is an Ok variant, it is returned unchanged
* - If this Result is a Fail variant, the fallback Result is returned
*
* @param fallback - The Result to return if this Result is Fail
* @returns This Result if Ok, or the fallback Result if Fail
*
* @example
* // Try multiple data sources in order
* const userData = getUserFromCache(userId)
* .orElse(getUserFromDatabase(userId))
* .orElse(getUserFromBackupService(userId));
*
* // Providing a default value as a Result
* const config = loadConfig()
* .orElse(ok(DEFAULT_CONFIG));
*
* // config is guaranteed to be Ok
* const configValue = config.unwrap(); // Safe, will never throw
*/
abstract orElse(fallback: IResult<TOk, TFail>): IResult<TOk, TFail>
/**
* Swaps the Ok and Fail values, creating a new Result with inversed variants.
*
* This method transforms:
* - An Ok Result into a Fail Result with the same value (now as an error)
* - A Fail Result into an Ok Result with the same error (now as a value)
*
* This can be useful for inverting logic or for protocols where the error case
* is actually the expected or desired outcome.
*
* @returns A new Result with the Ok and Fail variants swapped
*
* @example
* // Inverting validation logic
* const isInvalid = validateInput(input) // Returns Ok if valid, Fail if invalid
* .swap() // Returns Fail if valid, Ok if invalid
* .isOk(); // true if the input was invalid
*
* // Working with negative conditions
* const userNotFound = findUser(userId)
* .swap()
* .isOk(); // true if the user was not found
*
* // Converting between error domains
* checkPermission(user, resource) // Returns Ok(true) if permitted, Fail(error) if not
* .swap() // Returns Fail(true) if permitted, Ok(error) if not
* .map(error => ({ // Only runs for permission errors
* type: 'ACCESS_DENIED',
* message: `Access denied: ${error.message}`,
* resource
* }))
* .swap(); // Back to Ok for permitted, Fail for denied
*/
abstract swap(): IResult<TFail, TOk>
/**
* Combines this Result with another Result using a combining function.
*
* This method allows for working with two independent Results together:
* - If both Results are Ok, applies the function to both values and returns an Ok Result
* - If either Result is Fail, returns the first Fail Result encountered
*
* This is useful for combining data that comes from different sources where both
* are needed to proceed.
*
* @param other - Another Result to combine with this one
* @param fn - A function that combines the two Ok values
* @returns A new Result containing either the combined values or the first error
*
* @example
* // Combining user data and preferences that are loaded separately
* const userData = fetchUserData(userId);
* const userPrefs = fetchUserPreferences(userId);
*
* const userProfile = userData.zipWith(
* userPrefs,
* (data, prefs) => ({
* ...data,
* preferences: prefs,
* theme: prefs.theme || 'default'
* })
* );
*
* // Working with multiple API responses
* const orders = fetchOrders(userId);
* const payments = fetchPayments(userId);
*
* const combinedData = orders.zipWith(
* payments,
* (orderList, paymentList) => {
* return orderList.map(order => ({
* ...order,
* payments: paymentList.filter(p => p.orderId === order.id)
* }));
* }
* );
*/
abstract zipWith<U, R>(other: IResult<U, TFail>, fn: (a: TOk, b: U) => R): IResult<R, TFail>
/**
* Maps the Ok value of this Result to a Promise, and then flattens the resulting structure.
*
* This method allows for seamless integration with asynchronous code:
* - If this Result is an Ok variant, it applies the function to the contained value,
* resolves the Promise, and wraps the resolved value in a new Ok Result
* - If the Promise rejects, it returns a Fail Result with the rejection reason
* - If this Result is a Fail variant, it returns a Promise that resolves to the
* original Fail Result without calling the function
*
* @param fn - A function that takes the Ok value and returns a Promise
* @returns A Promise that resolves to a new Result
*
* @example
* // Chaining synchronous and asynchronous operations
* validateUser(userData)
* .flatMapPromise(user => saveUserToDatabase(user))
* .then(result => result.match({
* ok: savedUser => sendWelcomeEmail(savedUser),
* fail: error => logError("Failed to save user", error)
* }));
*
* // Multi-step asynchronous workflow
* function processOrder(orderData) {
* // Start with sync validation returning a Result
* return validateOrder(orderData)
* .flatMapPromise(order => {
* // Async payment processing
* return processPayment(order.paymentDetails).then(paymentResult => {
* if (!paymentResult.success) {
* throw new Error(`Payment failed: ${paymentResult.message}`);
* }
*
* // Async inventory check and allocation
* return allocateInventory(order.items).then(() => {
* // Return the processed order
* return {
* ...order,
* status: 'PAID',
* paymentId: paymentResult.id
* };
* });
* });
* })
* .flatMapPromise(paidOrder => {
* // Final database save
* return saveOrderToDatabase(paidOrder).then(orderId => {
* return { ...paidOrder, id: orderId };
* });
* });
* }
*/
abstract flatMapPromise<M>(fn: (val: TOk) => Promise<M>): Promise<IResult<M, TFail>>
/**
* Maps the Ok value of this Result to an Observable, and then flattens the resulting structure.
*
* This method allows for seamless integration with reactive code:
* - If this Result is an Ok variant, it applies the function to the contained value,
* subscribes to the Observable, and wraps the first emitted value in a new Ok Result
* - If the Observable errors, it returns a Fail Result with the error
* - If the Observable completes without emitting, it returns a Fail Result with the provided default error
* - If this Result is a Fail variant, it returns a Promise that resolves to the
* original Fail Result without calling the function
*
* @param fn - A function that takes the Ok value and returns an Observable
* @param defaultError - The error to use if the Observable completes without emitting
* @returns A Promise that resolves to a new Result
*
* @requires rxjs@^7.0
* @example
* // Chaining Result with reactive code
* validateUser(userData)
* .flatMapObservable(
* user => userService.save(user),
* new Error("Failed to save user")
* )
* .then(result => result.match({
* ok: savedUser => notifyUserCreated(savedUser),
* fail: error => logError("User creation failed", error)
* }));
*
* // Processing real-time data
* getSensorData()
* .flatMapObservable(
* config => sensorApi.connectAndGetReading(config),
* new Error("No sensor reading received")
* )
* .then(result => result.match({
* ok: reading => updateDashboard(reading),
* fail: error => showSensorError(error)
* }));
*/
abstract flatMapObservable<M>(
fn: (val: TOk) => import('rxjs').Observable<M>,
defaultError: TFail
): Promise<IResult<M, TFail>>
}
export class OkResult<TOk, TFail> extends Result<TOk, TFail> {
constructor(private readonly successValue: TOk) {
super()
}
/**
* Returns true as this is an Ok Result variant.
*
* This implementation satisfies the abstract method from the Result class
* and serves as a TypeScript type guard, allowing TypeScript to narrow the
* type to OkResult when `isOk()` returns true.
*
* @returns true (always, for OkResult instances)
*/
isOk(): this is OkResult<TOk, TFail> {
return true
}
/**
* Returns false as this is not a Fail Result variant.
*
* This implementation satisfies the abstract method from the Result class
* and serves as a TypeScript type guard that will never narrow the type
* to FailResult for an OkResult instance.
*
* @returns false (always, for OkResult instances)
*/
isFail(): this is FailResult<TOk, TFail> {
return false
}
/**
* Extracts the contained Ok value.
*
* Since this is an OkResult, this method safely returns the contained value
* without any risk of exceptions.
*
* @returns The contained Ok value
*/
unwrap(): TOk {
return this.successValue
}
/**
* Extracts the contained Ok value or returns a default.
*
* Since this is an OkResult, this method simply returns the contained
* value and ignores the default value parameter.
*
* @param opt The default value (not used in OkResult implementation)
* @returns The contained Ok value
*/
unwrapOr(): TOk {
return this.unwrap()
}
/**
* Attempts to extract the contained Fail value, but throws since this is an OkResult.
*
* This method is unsafe to call on an OkResult and will always throw an exception.
*
* @throws ReferenceError Always throws with message 'Cannot unwrap a success as a failure'
* @returns Never returns a value, always throws
*/
unwrapFail(): never {
throw new ReferenceError('Cannot unwrap a success as a failure')
}
/**
* Converts this Ok Result to a Some Maybe containing the success value.
*
* This method transforms the Ok Result into a Maybe, focusing on the success path.
* It always returns a Some Maybe for OkResult instances.
*
* @returns A Some Maybe containing the non-nullable success value
*/
maybeOk(): IMaybe<NonNullable<TOk>> {
return maybe(this.successValue as NonNullable<TOk>)
}
/**
* Converts this Ok Result to a None Maybe.
*
* This method transforms the Ok Result into a Maybe, focusing on the failure path.
* Since this is an OkResult with no error value, it always returns a None Maybe.
*
* @returns A None Maybe (always, for OkResult instances)
*/
maybeFail(): IMaybe<TFail> {
return none()
}
/**
* Applies the success branch of a pattern matching object to this Ok Result.
*
* This method provides a functional way to handle the Ok variant specifically.
* For OkResult instances, only the 'ok' function of the pattern is called.
*
* @typeParam M - The return type of the pattern matching functions
* @param fn - An object containing functions to handle each Result variant
* @returns The result of applying the 'ok' function to the contained value
*/
match<M>(fn: IResultMatchPattern<TOk, TFail, M>): M {
return fn.ok(this.successValue)
}
/**
* Maps the Ok value using the provided function.
*
* For OkResult instances, this transforms the contained value using the provided
* function and returns a new OkResult with the transformed value.
*
* @typeParam M - The type of the mapped value
* @param fn - A function that transforms the Ok value
* @returns A new OkResult with the transformed value
*/
map<M>(fn: (val: TOk) => M): IResult<M, TFail> {
return Result.ok<M, TFail>(fn(this.successValue))
}
/**
* Maps the Fail value using the provided function, which is a no-op for OkResult.
*
* For OkResult instances, this method ignores the mapping function since there is
* no error value to transform. It returns a new OkResult with the same success value
* but with the new error type parameter.
*
* @typeParam M - The type of the mapped error (only changes the type parameter)
* @param fn - A function that would transform the Fail value (not used in OkResult)
* @returns A new OkResult with the same success value and updated error type
*/
mapFail<M>(): IResult<TOk, M> {
return Result.ok(this.successValue)
}
/**
* Chains a function that returns another Result, applying it to the contained value.
*
* This is the monadic bind operation for Result. For OkResult instances, it applies
* the function to the contained value and returns the resulting Result directly.
*
* @typeParam M - The type of the value in the returned Result
* @param fn - A function that takes the Ok value and returns a new Result
* @returns The Result returned by applying the function to the contained value
*/
flatMap<M>(fn: (val: TOk) => IResult<M, TFail>): IResult<M, TFail> {
return fn(this.successValue)
}
/**
* Maps the success value to a Maybe, and flattens the resulting structure.
*
* Since this is an Ok Result, the function is applied to the contained value.
* The result depends on whether the Maybe is Some or None:
* - If Some: Returns an Ok Result with the unwrapped value
* - If None: Returns a Fail Result with the provided error
*
* This implementation follows the monadic bind operation pattern where we:
* 1. Apply the function to get a Maybe
* 2. Match on the Maybe to convert it back to a Result
*
* @param fn Function mapping the contained value to a Maybe
* @param err Error value to use if the Maybe is None
* @returns Either an Ok Result with the unwrapped value or a Fail Result with the provided error
*/
flatMapMaybe<M>(fn: (val: TOk) => IMaybe<M>, err: TFail): IResult<M, TFail> {
return fn(this.successValue).match({
some: (val) => Result.ok<M, TFail>(val),
none: () => Result.fail<M, TFail>(err)
})
}
/**
* Converts this Ok Result into a Fail Result using a transformation function.
*
* For OkResult instances, this method applies the function to the contained value
* to generate an error and returns a new Fail Result with that error.
*
* @param fn - A function that transforms the Ok value into a Fail value
* @returns A Fail Result with the error generated from the contained value
*/
toFailWhenOk(fn: (val: TOk) => TFail): IResult<TOk, TFail> {
return Result.fail(fn(this.successValue))
}
/**
* Converts this Ok Result into a Fail Result using a provided error value.
*
* For OkResult instances, this method ignores the contained value and returns
* a new Fail Result with the provided error value.
*
* @param val - The error value to use in the returned Fail Result
* @returns A