client/src/utils/result.ts (57 lines of code) (raw):
// ----- Imports ----- //
import { none, some } from "./option";
import type { Option } from "./option";
// ----- Types ----- //
enum ResultKind {
Ok,
Err,
}
type Ok<A> = {
kind: ResultKind.Ok;
value: A;
};
type Err<E> = {
kind: ResultKind.Err;
err: E;
};
/**
* Represents either a value or an error; it's either an `Ok` or an `Err`.
*/
type Result<E, A> = Err<E> | Ok<A>;
// ----- Constructors ----- //
const ok = <A>(a: A): Ok<A> => ({ kind: ResultKind.Ok, value: a });
const err = <E>(e: E): Err<E> => ({ kind: ResultKind.Err, err: e });
// ----- Functions ----- //
/**
* The method for turning a `Result<E, A>` into a plain value.
* If this is an `Err`, apply the first function to the error value and
* return the result. If this is an `Ok`, apply the second function to
* the value and return the result.
* @param f The function to apply if this is an `Err`
* @param g The function to apply if this is an `Ok`
* @param result The Result
* @example
* const flakyTaskResult: Result<string, number> = flakyTask(options);
*
* either(
* data => `We got the data! Here it is: ${data}`,
* error => `Uh oh, an error: ${error}`,
* )(flakyTaskResult)
*/
const either =
<E, A>(result: Result<E, A>) =>
<C>(f: (e: E) => C, g: (a: A) => C) =>
result.kind === ResultKind.Ok ? g(result.value) : f(result.err);
/**
* The companion to `map`.
* Applies a function to the error in `Err`, does nothing to an `Ok`.
* @param f The function to apply if this is an `Err`
* @param result The Result
*/
const mapError =
<E, A>(f: (e: E) => Result<E, A>) =>
(result: Result<E, A>): Result<E, A> =>
result.kind === ResultKind.Err ? f(result.err) : result;
/**
* Converts a `Result<E, A>` into an `Option<A>`. If the result is an
* `Ok` this will be a `Some`, if the result is an `Err` this will be
* a `None`.
* @param result The Result
*/
const toOption = <E, A>(result: Result<E, A>): Option<A> =>
result.kind === ResultKind.Ok ? some(result.value) : none;
/**
* Similar to `Option.map`.
* Applies a function to the value in an `Ok`, does nothing to an `Err`.
* @param f The function to apply if this is an `Ok`
* @param result The Result
*/
const map =
<A, B>(f: (a: A) => B) =>
<E>(result: Result<E, A>): Result<E, B> =>
result.kind === ResultKind.Ok ? ok(f(result.value)) : result;
/**
* Similar to `Option.andThen`. Applies to a `Result` a function that
* *also* returns a `Result`, and unwraps them to avoid nested `Result`s.
* Can be useful for stringing together operations that might fail.
* @example
* type RequestUser = number => Result<string, User>;
* type GetEmail = User => Result<string, string>;
*
* // Request fails: Err('Network failure')
* // Request succeeds, problem accessing email: Err('Email field missing')
* // Both succeed: Ok('email_address')
* andThen(getEmail)(requestUser(id))
*/
const andThen =
<E, A, B>(f: (a: A) => Result<E, B>) =>
(result: Result<E, A>): Result<E, B> =>
result.kind === ResultKind.Ok ? f(result.value) : result;
/**
* The return type of the `partition` function
*/
type Partitioned<E, A> = { errs: E[]; oks: A[] };
/**
* Takes a list of `Result`s and separates out the `Ok`s from the `Err`s.
* @param results A list of `Result`s
* @return {Partitioned} An object with two fields, one for the list of `Err`s
* and one for the list of `Ok`s
*/
const partition = <E, A>(results: Array<Result<E, A>>): Partitioned<E, A> =>
results.reduce(
({ errs, oks }: Partitioned<E, A>, result) =>
either<E, A>(result)<Partitioned<E, A>>(
(err) => ({ errs: [...errs, err], oks }),
(ok) => ({ errs, oks: [...oks, ok] })
),
{ errs: [], oks: [] }
);
// ----- Exports ----- //
export {
ResultKind,
ok,
err,
partition,
either,
mapError,
toOption,
map,
andThen,
};
export type { Result };