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