/** * 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_(() => 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 { * 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 { * 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"; import _wrap from "./wrap.ts"; export interface IResult { /** * 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 { * 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 extends IResult { readonly ok: true; readonly value: T; readonly error?: undefined; } export interface ResultErr extends IResult { 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_(() => divide(10, 0)); * if (result.ok) { * console.log("Result:", result.value); * } else { * console.error("Error:", result.error.message); * } * ``` */ try(fn: (...args: A) => Result, ...args: A): Result; try(fn: (...args: A) => Promise>, ...args: A): Promise>; try(fn: (...args: A) => Promise, ...args: A): Promise>; try(fn: (...args: A) => R | never, ...args: A): Result; /** * 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(promise: Promise>): Promise>; try(promise: Promise): Promise>; /** * 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(fn: (...args: A) => Result): (...args: A) => Result; wrap(fn: (...args: A) => Promise>): (...args: A) => Promise>; wrap(fn: (...args: A) => Promise): (...args: A) => Promise>; wrap(fn: (...args: A) => R | never): (...args: A) => Result; /** * 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 { * 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 = { 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 = ResultOk | ResultErr; export const Result = function Result(this: any, init: ResultLike) { if (!new.target) { throw new TypeError("Class constructor Result cannot be invoked without 'new'"); } const ins = this as Result; if (init.ok) { Object.defineProperties(ins, { ok: { value: true, enumerable: true }, value: { value: init.value, enumerable: true }, }); } else { Object.defineProperties(ins, { ok: { value: false, enumerable: true }, error: { value: init.error, enumerable: true }, }); } } as unknown as { new (init: ResultLike): Result; prototype: Result; } & ResultStatic; Result.prototype.unwrap = function unwrap( this: Result, onError: ((error: E) => T) | undefined = undefined ) { if (this.ok) { return this.value; } else if (onError) { return onError(this.error); } else { throw this.error; } }; Result.prototype.optional = function optional(this: Result) { return this.ok ? this.value : undefined; }; Result.try = function try_(fn: ((...args: any[]) => any) | Promise, ...args: any[]): any { if (typeof fn === "function") { try { const res = (fn as Function).call(void 0, ...args); if (typeof res?.then === "function") { return Result.try(res); } else { return res instanceof Result ? res : Ok(res); } } catch (error) { return Err(error); } } else { return Promise.resolve(fn) .then(value => value instanceof Result ? value : Ok(value)) .catch(error => Err(error)); } }; Result.wrap = function wrap(fn: ((...args: any[]) => any) | undefined = undefined): (...args: any[]) => any { if (typeof fn === "function") { return _wrap(fn, function (this, fn, ...args) { try { const res = fn.call(this, ...args); if (typeof res?.then === "function") { return Promise.resolve(res) .then(value => value instanceof Result ? value : Ok(value)) .catch(error => Err(error)); } else { return res instanceof Result ? res : Ok(res); } } catch (error) { return Err(error); } }); } else { // as a decorator return (...args: any[]) => { if (typeof args[1] === "object") { // new ES decorator since TypeScript 5.0 const [fn] = args as [(this: any, ...args: any[]) => any, DecoratorContext]; return Result.wrap(fn); } else { const [_ins, _prop, desc] = args as [any, string, TypedPropertyDescriptor]; desc.value = Result.wrap(desc.value); return; } }; } }; /** * Constructs a successful result with the given value. */ export const Ok: (value: T) => Result = (value) => new Result({ ok: true, value }); /** * Constructs a failed result with the given error. */ export const Err: (error: E) => Result = (error) => new Result({ ok: false, error }); /** * 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_(() => divide(10, 0)); * if (result.ok) { * console.log("Result:", result.value); * } else { * console.error("Error:", result.error.message); * } * ``` */ export const try_ = Result.try;