@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
325 lines (324 loc) • 10.4 kB
TypeScript
/**
* This module provides the {@link Result} type that represents the result of
* an operation that may succeed or fail, similar to the `Result` type in Rust,
* but with a more JavaScript-friendly API and removes unnecessary boilerplate.
*
* This module provides a {@link try_} function (alias of {@link Result.try}) to
* safely invoke a function that may throw and wraps the result in a `Result`
* object.
*
* This module also provides the {@link Ok} and {@link Err} functions as
* shorthands for constructing successful and failed results, respectively. Also,
* a {@link Result.wrap} function is provided to wrap a traditional function
* that may throw an error into a function that always returns a `Result` object.
*
* This module don't give up the error propagation mechanism in JavaScript, we
* can still use the `try...catch` statement to catch the error and handle it in
* a traditional way. Which is done by using the `unwrap()` method of the
* `Result` object to retrieve the value directly.
*
* @example
* ```ts
* // Safely invoke a function that may throw with `try_`.
* import { try_ } from "@ayonli/jsext/result";
*
* function divide(a: number, b: number): number {
* if (b === 0) {
* throw new RangeError("division by zero");
* }
*
* return a / b;
* }
*
* const result = try_<number, RangeError>(() => divide(10, 0));
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*
* @example
* ```ts
* // Wrap a traditional function that may throw with `Result.wrap`.
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* function divide(a: number, b: number): number {
* if (b === 0) {
* throw new RangeError("division by zero");
* }
*
* return a / b;
* }
*
* const safeDivide = Result.wrap(divide);
*
* const result = safeDivide(10, 0);
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*
* @example
* ```ts
* // Define a class method that always returns a `Result` object.
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* class Calculator {
* \@Result.wrap()
* divide(a: number, b: number): Result<number, RangeError> {
* if (b === 0) {
* return Err(new RangeError("division by zero"));
* }
*
* return Ok(a / b);
* }
* }
*
* const calc = new Calculator();
*
* const result = calc.divide(10, 0);
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*
* @example
* ```ts
* // Use the `unwrap()` method to retrieve the value directly.
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* function safeDivide(a: number, b: number): Result<number, RangeError> {
* if (b === 0) {
* return Err(new RangeError("division by zero"));
* }
*
* return Ok(a / b);
* }
*
* try {
* const value = safeDivide(10, 0).unwrap();
* console.log("Result:", value);
* } catch (error) {
* console.error("Error:", (error as RangeError).message);
* }
* ```
*
* @experimental
* @module
*/
import { MethodDecorator } from "./types.ts";
export interface IResult<T, E> {
/**
* Whether the result is successful.
*/
readonly ok: boolean;
/**
* The value of the result if `ok` is `true`.
*/
value?: T | undefined;
/**
* The error of the result if `ok` is `false`.
*/
error?: E | undefined;
/**
* Returns the value if the result is successful, otherwise throws the error,
* unless the error is handled and a fallback value is provided.
*
* @param onError Handles the error and provides a fallback value.
*
* * @example
* ```ts
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* function safeDivide(a: number, b: number): Result<number, RangeError> {
* if (b === 0) {
* return Err(new RangeError("division by zero"));
* }
*
* return Ok(a / b);
* }
*
* try {
* const value = safeDivide(10, 0).unwrap();
* console.log("Result:", value);
* } catch (error) {
* console.error("Error:", (error as RangeError).message);
* }
* ```
*/
unwrap(onError?: ((error: E) => T) | undefined): T;
/**
* Returns the value if the result is successful, otherwise returns
* `undefined`.
*/
optional(): T | undefined;
}
export interface ResultOk<T, E> extends IResult<T, E> {
readonly ok: true;
readonly value: T;
readonly error?: undefined;
}
export interface ResultErr<T, E> extends IResult<T, E> {
readonly ok: false;
readonly error: E;
readonly value?: undefined;
}
export interface ResultStatic {
/**
* Safely invokes a function that may throw and wraps the result in a `Result`
* object. If the returned value is already a `Result` object, it will be
* returned as is.
*
* @example
* ```ts
* import { try_ } from "@ayonli/jsext/result";
*
* function divide(a: number, b: number): number {
* if (b === 0) {
* throw new RangeError("division by zero");
* }
*
* return a / b;
* }
*
* const result = try_<number, RangeError>(() => divide(10, 0));
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*/
try<T, E = unknown, A extends any[] = any[]>(fn: (...args: A) => Result<T, E>, ...args: A): Result<T, E>;
try<T, E = unknown, A extends any[] = any[]>(fn: (...args: A) => Promise<Result<T, E>>, ...args: A): Promise<Result<T, E>>;
try<R, E = unknown, A extends any[] = any[]>(fn: (...args: A) => Promise<R | never>, ...args: A): Promise<Result<R, E>>;
try<R, E = unknown, A extends any[] = any[]>(fn: (...args: A) => R | never, ...args: A): Result<R, E>;
/**
* Resolves the promise's result in a `Result` object. If the fulfilled value
* is already a `Result` object, it will be resolved as is.
*/
try<T, E = unknown>(promise: Promise<Result<T, E>>): Promise<Result<T, E>>;
try<R, E = unknown>(promise: Promise<R | never>): Promise<Result<R, E>>;
/**
* Wraps a function that may throw and returns a new function that always
* returns a `Result` object. If the returned value is already a `Result`
* object, it will be returned as is.
*
* @example
* ```ts
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* function divide(a: number, b: number): number {
* if (b === 0) {
* throw new RangeError("division by zero");
* }
*
* return a / b;
* }
*
* const safeDivide = Result.wrap(divide);
*
* const result = safeDivide(10, 0);
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*/
wrap<A extends any[], T, E = unknown>(fn: (...args: A) => Result<T, E>): (...args: A) => Result<T, E>;
wrap<A extends any[], T, E = unknown>(fn: (...args: A) => Promise<Result<T, E>>): (...args: A) => Promise<Result<T, E>>;
wrap<A extends any[], R, E = unknown>(fn: (...args: A) => Promise<R | never>): (...args: A) => Promise<Result<R, E>>;
wrap<A extends any[], R, E = unknown>(fn: (...args: A) => R | never): (...args: A) => Result<R, E>;
/**
* Used as a decorator, make sure a method always returns a `Result`
* instance, even if it throws.
*
* @example
* ```ts
* import { Err, OK, Result } from "@ayonli/jsext/result";
*
* class Calculator {
* \@Result.wrap()
* divide(a: number, b: number): Result<number, RangeError> {
* if (b === 0) {
* return Err(new RangeError("division by zero"));
* }
*
* return Ok(a / b);
* }
* }
*
* const calc = new Calculator();
*
* const result = calc.divide(10, 0);
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*/
wrap(): MethodDecorator;
}
export type ResultLike<T, E = unknown> = {
ok: true;
value: T;
error?: undefined;
} | {
ok: false;
error: E;
value?: undefined;
};
/**
* Represents the result of an operation that may succeed or fail.
*/
export type Result<T, E = unknown> = ResultOk<T, E> | ResultErr<T, E>;
export declare const Result: {
new <T, E = unknown>(init: ResultLike<T, E>): Result<T, E>;
prototype: Result<unknown, unknown>;
} & ResultStatic;
/**
* Constructs a successful result with the given value.
*/
export declare const Ok: <T, E = unknown>(value: T) => Result<T, E>;
/**
* Constructs a failed result with the given error.
*/
export declare const Err: <E, T = never>(error: E) => Result<T, E>;
/**
* A alias for {@link Result.try}.
*
* @example
* ```ts
* import { try_ } from "@ayonli/jsext/result";
*
* function divide(a: number, b: number): number {
* if (b === 0) {
* throw new RangeError("division by zero");
* }
*
* return a / b;
* }
*
* const result = try_<number, RangeError>(() => divide(10, 0));
* if (result.ok) {
* console.log("Result:", result.value);
* } else {
* console.error("Error:", result.error.message);
* }
* ```
*/
export declare const try_: {
<T, E = unknown, A extends any[] = any[]>(fn: (...args: A) => Result<T, E>, ...args: A): Result<T, E>;
<T_1, E_1 = unknown, A_1 extends any[] = any[]>(fn: (...args: A_1) => Promise<Result<T_1, E_1>>, ...args: A_1): Promise<Result<T_1, E_1>>;
<R, E_2 = unknown, A_2 extends any[] = any[]>(fn: (...args: A_2) => Promise<R | never>, ...args: A_2): Promise<Result<R, E_2>>;
<R_1, E_3 = unknown, A_3 extends any[] = any[]>(fn: (...args: A_3) => R_1, ...args: A_3): Result<R_1, E_3>;
<T_2, E_4 = unknown>(promise: Promise<Result<T_2, E_4>>): Promise<Result<T_2, E_4>>;
<R_2, E_5 = unknown>(promise: Promise<R_2>): Promise<Result<R_2, E_5>>;
};