Created
January 18, 2025 12:47
-
-
Save sambacha/3f8b2c7c10430b41b5d4cd43c358da11 to your computer and use it in GitHub Desktop.
TypeScript implementation of the Result monad pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: UPL-1.0 OR BSD-3 | |
/** | |
* @file LibResult.ts | |
* @version 1.0.0 | |
* @packageDocumentation | |
* | |
* A TypeScript implementation of the Result monad pattern, providing a type-safe way | |
* to handle operations that might fail. This pattern helps eliminate null checks | |
* and provides a more functional approach to error handling. | |
* | |
* @example | |
* ```typescript | |
* // Basic usage | |
* const divide = (a: number, b: number): Result<number> => { | |
* if (b === 0) return Result.err("Division by zero"); | |
* return Result.ok(a / b); | |
* }; | |
* | |
* const result = divide(10, 2) | |
* .map(result => result * 2) | |
* .fold( | |
* value => console.log(`Success: ${value}`), | |
* error => console.error(`Error: ${error}`) | |
* ); | |
* ``` | |
*/ | |
/** | |
* Interface representing a successful result. | |
* @template T - The type of the successful result. | |
* @since 1.0.0 | |
* @category Interfaces | |
*/ | |
export interface IResultOk<T> { | |
res: T; | |
err?: undefined; | |
} | |
/** | |
* Interface representing a failed result. | |
* @template E - The type of the error. | |
* @since 1.0.0 | |
* @category Interfaces | |
*/ | |
export interface IResultErr<E = string> { | |
res?: undefined; | |
err: E; | |
} | |
/** | |
* Type alias for a result, which can either be successful or failed. | |
* @template T - The type of the successful result. | |
* @template E - The type of the error. | |
* @since 1.0.0 | |
* @category Types | |
*/ | |
export type TResult<T, E = string> = IResultOk<T> | IResultErr<E>; | |
/** | |
* Class for encapsulating a result, which can either be a success or failure. | |
* Provides utility methods for handling result values in a type-safe manner. | |
* | |
* This class implements the Result monad pattern, which is useful for: | |
* - Handling operations that might fail | |
* - Chaining multiple operations together | |
* - Providing type-safe error handling | |
* | |
* @template T - The type of the successful result. | |
* @template E - The type of the error. | |
* @since 1.0.0 | |
* @category Classes | |
* | |
* @example | |
* ```typescript | |
* // Chaining multiple operations | |
* const result = await Result.ok(user) | |
* .andAsync(user => validateUser(user)) | |
* .andAsync(user => saveToDatabase(user)) | |
* .fold( | |
* user => ({ success: true, user }), | |
* error => ({ success: false, error }) | |
* ); | |
* ``` | |
*/ | |
export class Result<T, E = string> { | |
/** @private The underlying result value */ | |
private readonly val: TResult<T, E>; | |
/** | |
* Creates a successful result. | |
* @param value - The value of the successful result. | |
* @returns A new Result instance containing the successful value. | |
* @throws Never | |
* @category Static Methods | |
* | |
* @example | |
* ```typescript | |
* const result = Result.ok(42); | |
* console.log(result.isSuccess()); // true | |
* ``` | |
*/ | |
public static ok<T>(value: T): Result<T> { | |
return new Result({ res: value }); | |
} | |
/** | |
* Creates a failed result. | |
* @param error - The error message of the failed result. | |
* @returns A new Result instance containing the error. | |
* @throws Never | |
* @category Static Methods | |
* | |
* @example | |
* ```typescript | |
* const result = Result.err("Something went wrong"); | |
* console.log(result.isFailure()); // true | |
* ``` | |
*/ | |
public static err<T, E = string>(error: E): Result<T, E> { | |
return new Result({ err: error }); | |
} | |
/** | |
* Converts an existing result type into a `Result` instance. | |
* @param value - The existing result to convert. | |
* @returns A new Result instance wrapping the provided value. | |
* @throws Never | |
* @category Static Methods | |
* | |
* @example | |
* ```typescript | |
* const result: TResult<number> = { res: 42 }; | |
* const wrapped = Result.from(result); | |
* ``` | |
*/ | |
public static from<T, E>(value: TResult<T, E>): Result<T, E> { | |
return new Result(value); | |
} | |
/** | |
* Constructs a `Result` instance. | |
* @param value - The underlying result value. | |
* @private | |
*/ | |
constructor(value: TResult<T, E>) { | |
this.val = value; | |
} | |
/** | |
* Maps the successful result value to a new value using the provided function. | |
* @template U - The type of the new result value. | |
* @param fn - A function that transforms the successful result value. | |
* @example | |
* const result = Result.ok(10).map(x => x * 2); // Result.ok(20) | |
*/ | |
public map<U>(fn: (value: T) => U): Result<U, E> { | |
return this.isOk(this.val) | |
? Result.ok(fn(this.val.res)) | |
: Result.err(this.val.err); | |
} | |
/** | |
* Maps the successful result value to a new value asynchronously. | |
* @template U - The type of the new result value. | |
* @param fn - An asynchronous function that transforms the successful result value. | |
* @example | |
* const result = await Result.ok(10).mapAsync(async x => x * 2); // Result.ok(20) | |
*/ | |
public async mapAsync<U>(fn: (value: T) => Promise<U>): Promise<Result<U, E>> { | |
return this.isOk(this.val) | |
? Result.ok(await fn(this.val.res)) | |
: Result.err(this.val.err); | |
} | |
/** | |
* Chains the result to another result-producing function. | |
* @template U - The type of the new result value. | |
* @param fn - A function that takes the successful result value and returns a new `Result`. | |
* @example | |
* const result = Result.ok(10).and(x => Result.ok(x * 2)); // Result.ok(20) | |
*/ | |
public and<U>(fn: (arg: T) => Result<U, E>): Result<U, E> { | |
return this.isOk(this.val) | |
? fn(this.val.res) | |
: Result.err(this.val.err); | |
} | |
/** | |
* Chains the result to another result-producing function asynchronously. | |
* @template U - The type of the new result value. | |
* @param fn - An asynchronous function that returns a new `Result`. | |
* @example | |
* const result = await Result.ok(10).andAsync(async x => Result.ok(x * 2)); // Result.ok(20) | |
*/ | |
public async andAsync<U>( | |
fn: (arg: T) => Promise<Result<U, E>> | |
): Promise<Result<U, E>> { | |
return this.isOk(this.val) | |
? await fn(this.val.res) | |
: Result.err(this.val.err); | |
} | |
/** | |
* Provides an alternative result if the current result is a failure. | |
* @template U - The type of the alternative result value. | |
* @param fn - A function that takes the error message and returns an alternative `Result`. | |
* @example | |
* const result = Result.err("Error").orElse(err => Result.ok(42)); // Result.ok(42) | |
*/ | |
public orElse<U>(fn: (error: E) => Result<U, E>): Result<T | U, E> { | |
return this.isOk(this.val) | |
? this | |
: fn(this.val.err); | |
} | |
/** | |
* Handles both success and error cases in a single function. | |
* @template U - The return type of the handling functions. | |
* @param onSuccess - A function that handles the success case. | |
* @param onError - A function that handles the error case. | |
* @example | |
* const result = Result.ok(42).fold( | |
* res => `Success: ${res}`, | |
* err => `Error: ${err}` | |
* ); // "Success: 42" | |
*/ | |
public fold<U>(onSuccess: (value: T) => U, onError: (err: E) => U): U { | |
return this.isOk(this.val) | |
? onSuccess(this.val.res) | |
: onError(this.val.err); | |
} | |
/** | |
* Unwraps the successful result value or throws an error if the result is a failure. | |
* @throws An error if the result is a failure. | |
* @returns The successful result value. | |
* @example | |
* const value = Result.ok(42).unwrap(); // 42 | |
*/ | |
public unwrap(): T { | |
if (this.isOk(this.val)) { | |
return this.val.res; | |
} | |
throw new Error(this.val.err as string); | |
} | |
/** | |
* Checks if the result is a success. | |
* @returns `true` if the result is a success, otherwise `false`. | |
*/ | |
public isSuccess(): boolean { | |
return this.isOk(this.val); | |
} | |
/** | |
* Checks if the result is a failure. | |
* @returns `true` if the result is a failure, otherwise `false`. | |
*/ | |
public isFailure(): boolean { | |
return !this.isOk(this.val); | |
} | |
/** | |
* Retrieves the underlying value of the result. | |
* @returns The underlying result value. | |
*/ | |
public toVal(): TResult<T, E> { | |
return this.val; | |
} | |
private isOk(val: TResult<T, E>): val is IResultOk<T> { | |
return !val.err; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment