UNPKG

true-myth

Version:

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

417 lines (385 loc) 13.5 kB
/** {@include doc/result.md} @module */ import Unit from './unit.js'; import { curry1, safeToString } from './-private/utils.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); } /** Method variant for {@linkcode orElse} */ orElse(orElseFn) { return (this.repr[0] === 'Ok' ? this : 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); } /** Method variant for {@linkcode andThen} */ andThen(andThenFn) { return (this.repr[0] === 'Ok' ? andThenFn(this.repr[1]) : this); } /** 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]); } /** 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]; return variant === 'Ok' ? { variant, value: this.repr[1] } : { variant, error: this.repr[1] }; } /** 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))); } 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); } /** 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 const toJSON = (result) => { return result.toJSON(); }; 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) { let errorHandler = handleErr ?? ((e) => e); return (...params) => tryOrElse(errorHandler, () => fn(...params)); } /** Determine whether an item is an instance of {@linkcode Result}. @param item The item to check. */ export function isInstance(item) { return item instanceof ResultImpl; } /** 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