true-myth
Version:
A library for safe functional programming in JavaScript, with first-class support for TypeScript
417 lines (385 loc) • 13.5 kB
JavaScript
/**
{@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