UNPKG

true-myth

Version:

A library for safe functional programming in JavaScript, with first-class support for TypeScript

629 lines (568 loc) 21.4 kB
/** A {@linkcode Result Result<T, E>} is a type representing the value result of a synchronous operation which may fail, with a successful value of type `T` or an error of type `E`. If the result is a success, it is {@linkcode Ok Ok(value)}. If the result is a failure, it is {@linkcode Err Err(reason)}. For a deep dive on the type, see [the guide](/guide/understanding/result.md). @module */ import { curry1, identity, safeToString } from './-private/utils.js'; import Unit from './unit.js'; /** Discriminant for {@linkcode Ok} and {@linkcode Err} variants of the {@linkcode Result} type. You can use the discriminant via the `variant` property of `Result` instances if you need to match explicitly on it. */ export const Variant = { Ok: 'Ok', Err: 'Err', }; // Defines the *implementation*, but not the *types*. See the exports below. class ResultImpl { repr; constructor(repr) { this.repr = repr; } static ok(value) { // We produce `Unit` *only* in the case where no arguments are passed, so // that we can allow `undefined` in the cases where someone explicitly opts // into something like `Result<undefined, Blah>`. return arguments.length === 0 ? new ResultImpl(['Ok', Unit]) : // SAFETY: TS does not understand that the arity check above accounts for // the case where the value is not passed. new ResultImpl(['Ok', value]); } static err(error) { // We produce `Unit` *only* in the case where no arguments are passed, so // that we can allow `undefined` in the cases where someone explicitly opts // into something like `Result<undefined, Blah>`. return arguments.length === 0 ? new ResultImpl(['Err', Unit]) : // SAFETY: TS does not understand that the arity check above accounts for // the case where the value is not passed. new ResultImpl(['Err', error]); } /** Distinguish between the {@linkcode Variant.Ok} and {@linkcode Variant.Err} {@linkcode Variant variants}. */ get variant() { return this.repr[0]; } /** The wrapped value. @throws if you access when the {@linkcode Result} is not {@linkcode Ok} */ get value() { if (this.repr[0] === Variant.Err) { throw new Error('Cannot get the value of Err'); } return this.repr[1]; } /** The wrapped error value. @throws if you access when the {@linkcode Result} is not {@linkcode Err} */ get error() { if (this.repr[0] === Variant.Ok) { throw new Error('Cannot get the error of Ok'); } return this.repr[1]; } /** Is the {@linkcode Result} an {@linkcode Ok}? */ get isOk() { return this.repr[0] === Variant.Ok; } /** Is the `Result` an `Err`? */ get isErr() { return this.repr[0] === Variant.Err; } /** Method variant for {@linkcode map} */ map(mapFn) { return (this.repr[0] === 'Ok' ? Result.ok(mapFn(this.repr[1])) : this); } /** Method variant for {@linkcode mapOr} */ mapOr(orU, mapFn) { return this.repr[0] === 'Ok' ? mapFn(this.repr[1]) : orU; } /** Method variant for {@linkcode mapOrElse} */ mapOrElse(orElseFn, mapFn) { return this.repr[0] === 'Ok' ? mapFn(this.repr[1]) : orElseFn(this.repr[1]); } /** Method variant for {@linkcode match} */ match(matcher) { return this.repr[0] === 'Ok' ? matcher.Ok(this.repr[1]) : matcher.Err(this.repr[1]); } /** Method variant for {@linkcode mapErr} */ mapErr(mapErrFn) { return (this.repr[0] === 'Ok' ? this : Result.err(mapErrFn(this.repr[1]))); } /** Method variant for {@linkcode or} */ or(orResult) { return (this.repr[0] === 'Ok' ? this : orResult); } orElse(orElseFn) { return this.repr[0] === 'Ok' ? this.cast() : orElseFn(this.repr[1]); } /** Method variant for {@linkcode and} */ and(mAnd) { // (r.isOk ? andResult : err<U, E>(r.error)) return (this.repr[0] === 'Ok' ? mAnd : this); } andThen(andThenFn) { return this.repr[0] === 'Ok' ? andThenFn(this.repr[1]) : this.cast(); } /** Method variant for {@linkcode unwrapOr} */ unwrapOr(defaultValue) { return this.repr[0] === 'Ok' ? this.repr[1] : defaultValue; } /** Method variant for {@linkcode unwrapOrElse} */ unwrapOrElse(elseFn) { return this.repr[0] === 'Ok' ? this.repr[1] : elseFn(this.repr[1]); } /** Run a side effect with the wrapped value without modifying the {@linkcode Result}. This is useful for performing actions like logging, debugging, or other “side effects” external to the wrapped value. (**Note:** You should *never* mutate the value in the callback. Doing so will be extremely surprising to callers.) The function is only called if the `Result` is {@linkcode Ok}, and the original `Result` is returned unchanged for further chaining. ```ts import * as result from 'true-myth/result'; const double = (n: number) => n * 2; const log = (value: unknown) => console.log(value); // Logs `42` then `84`, and returns `Ok(84)`. result.ok<number, string>(42).inspect(log).map(double).inspect(log); // Does not log anything, and returns `Err('error')`. result.err<number, string>('error').inspect(log).map(double).inspect(log); ``` @param fn The function to call with the wrapped value, only called for `Ok`. @returns The original `Result`, unchanged */ inspect(fn) { if (this.repr[0] === 'Ok') { fn(this.repr[1]); } return this; } /** Run a side effect with a wrapped error value without modifying the {@linkcode Result}. This is useful for performing actions like logging, debugging, or other “side effects” external to the wrapped value. (**Note:** You should *never* mutate the value in the callback. Doing so will be extremely surprising to callers.) The function is only called if the `Result` is {@linkcode Err}, and the original `Result` is returned unchanged for further chaining. ```ts import * as result from 'true-myth/result'; const logError = (error: unknown) => console.logError('Got error:', error); result.err<number, string>('error') .inspectErr((error) => console.log('Got error:', error)) .mapErr((e) => e.toUpperCase()); // Logs: "Got error: error" // Returns: Err('ERROR') result.ok<number, string>(42) .inspectErr((error) => console.log('Got error:', error)) .mapErr((e) => e.toUpperCase()); // Logs nothing // Returns: Ok(42) ``` @param fn The function to call with the error value, only called for `Err`. @returns The original Result, unchanged */ inspectErr(fn) { if (this.repr[0] === 'Err') { fn(this.repr[1]); } return this; } /** Method variant for {@linkcode toString} */ toString() { return `${this.repr[0]}(${safeToString(this.repr[1])})`; } /** Method variant for {@linkcode toJSON} */ toJSON() { const variant = this.repr[0]; if (variant === Variant.Ok) { const value = isInstance(this.repr[1]) ? this.repr[1].toJSON() : this.repr[1]; return { variant, value }; } else { const error = isInstance(this.repr[1]) ? this.repr[1].toJSON() : this.repr[1]; return { variant, error }; } } /** Method variant for {@linkcode equals} */ equals(comparison) { // SAFETY: these casts are stripping away the `Ok`/`Err` distinction and // simply testing what `comparison` *actually* is, which is always an // instance of `ResultImpl` (the same as this method itself). return (this.repr[0] === comparison.repr[0] && this.repr[1] === comparison.repr[1]); } /** Method variant for {@linkcode ap} */ ap(r) { return r.andThen((val) => this.map((fn) => fn(val))); } /** Given a nested `Result`, remove one layer of nesting. For example, given a `Result<Result<string, E2>, E1>`, the resulting type after using this method will be `Result<string, E1 | E2>`. ## Note This method only works when the value wrapped in `Result` is another `Result`. If you have a `Result<string, E>` or `Result<number, E>`, this method won't work. If you have a `Result<Result<string, E2>, E1>`, then you can call `.flatten()` to get back a `Result<string, E1 | E2>`. ## Examples ```ts import * as result from 'true-myth/result'; const nested = result.ok(result.ok('hello')); const flattened = nested.flatten(); // Result<string, never> console.log(flattened); // Ok('hello') const nestedError = result.ok(result.err('inner error')); const flattenedError = nestedError.flatten(); // Result<never, string> console.log(flattenedError); // Err('inner error') const errorNested = result.err<Result<string, string>, string>('outer error'); const flattenedOuter = errorNested.flatten(); // Result<string, string> console.log(flattenedOuter); // Err('outer error') ``` */ // NOTE: it is necessary to express the type constraint on the `this` value // like this, rather than like `this: Result<Result<T, E2>, E1>`, to avoid // producing a `Result<Result<Result<T, E2>, E1>, E2>` at call sites with the // wrapped value. flatten() { return this.andThen(identity); } cast() { return this; } } export function tryOr(error, callback) { const op = (cb) => { try { return ok(cb()); } catch { return err(error); } }; return curry1(op, callback); } /** Create an instance of {@linkcode Ok}. If you need to create an instance with a specific type (as you do whenever you are not constructing immediately for a function return or as an argument to a function), you can use a type parameter: ```ts const yayNumber = Result.ok<number, string>(12); ``` Note: passing nothing, or passing `null` or `undefined` explicitly, will produce a `Result<Unit, E>`, rather than producing the nonsensical and in practice quite annoying `Result<null, string>` etc. See {@linkcode Unit} for more. ```ts const normalResult = Result.ok<number, string>(42); const explicitUnit = Result.ok<Unit, string>(Unit); const implicitUnit = Result.ok<Unit, string>(); ``` In the context of an immediate function return, or an arrow function with a single expression value, you do not have to specify the types, so this can be quite convenient. ```ts type SomeData = { //... }; const isValid = (data: SomeData): boolean => { // true or false... } const arrowValidate = (data: SomeData): Result<Unit, string> => isValid(data) ? Result.ok() : Result.err('something was wrong!'); function fnValidate(data: someData): Result<Unit, string> { return isValid(data) ? Result.ok() : Result.err('something was wrong'); } ``` @template T The type of the item contained in the `Result`. @param value The value to wrap in a `Result.Ok`. */ export const ok = ResultImpl.ok; /** Is the {@linkcode Result} an {@linkcode Ok}? @template T The type of the item contained in the `Result`. @param result The `Result` to check. @returns A type guarded `Ok`. */ export function isOk(result) { return result.isOk; } /** Is the {@linkcode Result} an {@linkcode Err}? @template T The type of the item contained in the `Result`. @param result The `Result` to check. @returns A type guarded `Err`. */ export function isErr(result) { return result.isErr; } /** Create an instance of {@linkcode Err}. If you need to create an instance with a specific type (as you do whenever you are not constructing immediately for a function return or as an argument to a function), you can use a type parameter: ```ts const notString = Result.err<number, string>('something went wrong'); ``` Note: passing nothing, or passing `null` or `undefined` explicitly, will produce a `Result<T, Unit>`, rather than producing the nonsensical and in practice quite annoying `Result<null, string>` etc. See {@linkcode Unit} for more. ```ts const normalResult = Result.err<number, string>('oh no'); const explicitUnit = Result.err<number, Unit>(Unit); const implicitUnit = Result.err<number, Unit>(); ``` In the context of an immediate function return, or an arrow function with a single expression value, you do not have to specify the types, so this can be quite convenient. ```ts type SomeData = { //... }; const isValid = (data: SomeData): boolean => { // true or false... } const arrowValidate = (data: SomeData): Result<number, Unit> => isValid(data) ? Result.ok(42) : Result.err(); function fnValidate(data: someData): Result<number, Unit> { return isValid(data) ? Result.ok(42) : Result.err(); } ``` @template T The type of the item contained in the `Result`. @param E The error value to wrap in a `Result.Err`. */ export const err = ResultImpl.err; export function tryOrElse(onError, callback) { const op = (cb) => { try { return ok(cb()); } catch (e) { return err(onError(e)); } }; return curry1(op, callback); } export function map(mapFn, result) { const op = (r) => r.map(mapFn); return curry1(op, result); } export function mapOr(orU, mapFn, result) { function fullOp(fn, r) { return r.mapOr(orU, fn); } function partialOp(fn, curriedResult) { return curriedResult !== undefined ? fullOp(fn, curriedResult) : (extraCurriedResult) => fullOp(fn, extraCurriedResult); } return mapFn === undefined ? partialOp : result === undefined ? partialOp(mapFn) : partialOp(mapFn, result); } export function mapOrElse(orElseFn, mapFn, result) { function fullOp(fn, r) { return r.mapOrElse(orElseFn, fn); } function partialOp(fn, curriedResult) { return curriedResult !== undefined ? fullOp(fn, curriedResult) : (extraCurriedResult) => fullOp(fn, extraCurriedResult); } return mapFn === undefined ? partialOp : result === undefined ? partialOp(mapFn) : partialOp(mapFn, result); } export function mapErr(mapErrFn, result) { const op = (r) => r.mapErr(mapErrFn); return curry1(op, result); } export function and(andResult, result) { const op = (r) => r.and(andResult); return curry1(op, result); } export function andThen(thenFn, result) { const op = (r) => r.andThen(thenFn); return curry1(op, result); } export function or(defaultResult, result) { const op = (r) => r.or(defaultResult); return curry1(op, result); } export function orElse(elseFn, result) { const op = (r) => r.orElse(elseFn); return curry1(op, result); } export function unwrapOr(defaultValue, result) { const op = (r) => r.unwrapOr(defaultValue); return curry1(op, result); } export function unwrapOrElse(orElseFn, result) { const op = (r) => r.unwrapOrElse(orElseFn); return curry1(op, result); } export function inspect(fn, result) { const op = (r) => r.inspect(fn); return curry1(op, result); } export function inspectErr(fn, result) { const op = (r) => r.inspectErr(fn); return curry1(op, result); } /** Create a `String` representation of a {@linkcode Result} instance. An {@linkcode Ok} instance will be `Ok(<representation of the value>)`, and an {@linkcode Err} instance will be `Err(<representation of the error>)`, where the representation of the value or error is simply the value or error's own `toString` representation. For example: call | output --------------------------------- | ---------------------- `toString(ok(42))` | `Ok(42)` `toString(ok([1, 2, 3]))` | `Ok(1,2,3)` `toString(ok({ an: 'object' }))` | `Ok([object Object])`n `toString(err(42))` | `Err(42)` `toString(err([1, 2, 3]))` | `Err(1,2,3)` `toString(err({ an: 'object' }))` | `Err([object Object])` @template T The type of the wrapped value; its own `.toString` will be used to print the interior contents of the `Just` variant. @param result The value to convert to a string. @returns The string representation of the `Maybe`. */ export const toString = (result) => { return result.toString(); }; /** * Create an `Object` representation of a {@linkcode Result} instance. * * Useful for serialization. `JSON.stringify()` uses it. * * @param result The value to convert to JSON * @returns The JSON representation of the `Result` */ export function toJSON(result) { return result.toJSON(); } /** * Given a {@linkcode ResultJSON} object, convert it into a {@linkcode Result}. * * Note that this is not designed for parsing data off the wire, but requires * you to have *already* parsed it into the {@linkcode ResultJSON} format. * * @param {ResultJSON<T,E>} json The value to convert from a JSON object. * @returns {Result<T,E>} The converted `Result` type. */ export function fromJSON(json) { return json.variant === Variant.Ok ? ok(json.value) : err(json.error); } export function match(matcher, result) { const op = (r) => r.mapOrElse(matcher.Err, matcher.Ok); return curry1(op, result); } export function equals(resultB, resultA) { const op = (rA) => rA.equals(resultB); return curry1(op, resultA); } export function ap(resultFn, result) { const op = (r) => resultFn.ap(r); return curry1(op, result); } export function safe(fn, handleErr) { const errorHandler = handleErr ?? ((e) => e); return (...params) => tryOrElse(errorHandler, () => fn(...params)); } export function isInstance(item) { return item instanceof ResultImpl; } export function all(results) { const oks = new Array(); for (const result of results) { if (result.isErr) { return Result.err(result.error); } oks.push(result.value); } return Result.ok(oks); } export function transposeAll(results) { const oks = []; const errs = []; for (const result of results) { if (result.isErr) { errs.push(result.error); } else if (errs.length === 0) { oks.push(result.value); } } return errs.length === 0 ? Result.ok(oks) : Result.err(errs); } export function transposeAny(results) { if (results.length === 0) { return Result.err([]); } const errs = []; for (const result of results) { if (result.isOk) { return Result.ok(result.value); } else { errs.push(result.error); } } return Result.err(errs); } /** Given a nested `Result`, remove one layer of nesting. For example, given a `Result<Result<string, E2>, E1>`, the resulting type after using this function will be `Result<string, E1 | E2>`. ## Note This function only works when the value wrapped in `Result` is another `Result`. If you have a `Result<string, E>` or `Result<number, E>`, this function won't work. If you have a `Result<Result<string, E2>, E1>`, then you can call `result.flatten(theResult)` to get back a `Result<string, E1 | E2>`. ## Examples ```ts import * as result from 'true-myth/result'; const nested = result.ok(result.ok('hello')); const flattened = result.flatten(nested); // Result<string, never> console.log(flattened); // Ok('hello') const nestedError = result.ok(result.err('inner error')); const flattenedError = result.flatten(nestedError); // Result<never, string> console.log(flattenedError); // Err('inner error') const errorNested = result.err<Result<string, string>, string>('outer error'); const flattenedOuter = result.flatten(errorNested); // Result<string, string> console.log(flattenedOuter); // Err('outer error') ``` */ export function flatten(nested) { // Uses `andThen` directly rather than calling `.flatten()` to avoid an extra // function dispatch. return nested.andThen(identity); } // Duplicate documentation because it will show up more nicely when rendered in // TypeDoc than if it applies to only one or the other; using `@inheritdoc` will // also work but works less well in terms of how editors render it (they do not // process that “directive” in general). /** A `Result` represents success ({@linkcode Ok}) or failure ({@linkcode Err}). The behavior of this type is checked by TypeScript at compile time, and bears no runtime overhead other than the very small cost of the container object. @class */ export const Result = ResultImpl; export default Result; //# sourceMappingURL=result.js.map