apps-rendering/src/result.ts (96 lines of code) (raw):
// ----- Imports ----- //
import type { Option } from '../vendor/@guardian/types/index';
import { none, some } from '../vendor/@guardian/types/index';
import { Optional } from 'optional';
// ----- Classes ----- //
/**
* The return type of the {@linkcode Result.partition} method.
*/
type Partitioned<E, A> = { errs: E[]; oks: A[] };
/**
* Represents either a value or an error; it's either an `Ok` or an `Err`.
*/
abstract class Result<E, A> {
// ----- Abstract Methods
/**
* Like {@linkcode map} but applies to a {@linkcode 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.
* It's the same as {@linkcode Array.flatMap}, which `map`s a function that
* returns an `Array`, and then "flattens" the resulting `Array<Array<A>>`.
* @param f The function to apply
* @returns {Result<E, B>} A new {@linkcode Result}
* @example
* const requestUser = (id: number): Result<string, User> => {...};
* const getEmail = (user: User): Result<string, string> => {...};
*
* // Request fails: Err('Network failure')
* // Request succeeds, problem accessing email: Err('Email field missing')
* // Both succeed: Ok('email_address')
* requestUser(id).flatMap(getEmail);
*/
abstract flatMap<B>(f: (a: A) => Result<E, B>): Result<E, B>;
/**
* The method for turning a {@linkcode Result} 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`
* @returns {C} The result of applying either `f` or `g`
* @example
* const flakyTaskResult: Result<string, number> =
* flakyTask(options);
*
* flakyTestResult.either(
* error => `Uh oh, an error: ${error}`,
* data => `We got the data! Here it is: ${data}`,
* )
*/
abstract either<C>(f: (e: E) => C, g: (a: A) => C): C;
/**
* The companion to {@linkcode map}.
* Applies a function to the error in `Err`, does nothing to an `Ok`.
* @param g The function to apply if this is an `Err`
* @returns {Result<E, F>} A new {@linkcode Result} with a different error
*/
abstract mapError<F>(g: (e: E) => F): Result<F, A>;
/**
* Converts a {@linkcode Result} into an {@linkcode Optional}. If the result
* is an `Ok` this will be a `Some`, if the result is an `Err` this will be
* a `None`.
* @returns {Optional<A>} An {@linkcode Optional}
*/
abstract toOptional(): Optional<A>;
/**
* Temporary method to convert to the old `Option` type. Same functionality
* as {@linkcode toOptional} but with `Option`.
*/
abstract toOption(): Option<A>;
/**
* Checks if a {@linkcode Result} is an `Ok`. Can be used in type guards to
* narrow the type and get access to the `value` inside an `Ok`.
* See also {@linkcode isErr}.
* @returns {boolean} A type predicate
* @example
* const name: Result<string, string> = Result.ok('CP Scott');
*
* console.log(name.value); // Type Error: 'value' does not exist
*
* if (name.isOk()) {
* console.log(name.value); // Works!
* }
*/
abstract isOk(): this is Ok<E, A>;
/**
* Checks if a {@linkcode Result} is an `Err`. Can be used in type guards to
* narrow the type and get access to the `error` inside an `Err`.
* See also {@linkcode isOk}.
* @returns {boolean} A type predicate
* @example
* const name: Result<string, string> = Result.err('Missing name!');
*
* console.log(name.error); // Type Error: 'value' does not exist
*
* if (name.isErr()) {
* console.log(name.error); // Works!
* }
*/
abstract isErr(): this is Err<E, A>;
// ----- Static Methods
/**
* Takes a value and wraps it up into an `Ok`.
* @example
* const maybeName: Result<string, string> =
* Result.ok('CP Scott');
*/
static ok<E, A>(a: A): Result<E, A> {
return new Ok(a);
}
/**
* Takes an error value and wraps it up into an `Err`.
* @example
* const maybeName: Result<string, string> =
* Result.err('Missing name!');
*/
static err<E, A>(e: E): Result<E, A> {
return new Err(e);
}
/**
* Converts an operation that might throw into a {@linkcode Result}.
* @param f The operation that might throw
* @param error The error to return if the operation throws
* @example
* Result.fromUnsafe(
* () => new URL('https://www.theguardian.com'),
* 'Could not create the URL',
* )
*/
static fromUnsafe<E, A>(f: () => A, error: E): Result<E, A> {
try {
return Result.ok(f());
} catch (_) {
return Result.err(error);
}
}
/**
* Takes a list of {@linkcode Result|Results} and separates out the `Ok`s
* from the `Err`s.
* @param results A list of `Result`s
* @return {Partitioned} A {@linkcode Partitioned} object with two fields,
* one for the list of `Err`s and one for the list of `Ok`s
* @example
* const results: Result<string, number>[] = ...;
*
* const partitioned = Result.partition(results);
* console.log(`Successes: ${partitioned.oks}`);
* console.log(`Errors: ${partitioned.errs}`);
*/
static partition<E, A>(results: Array<Result<E, A>>): Partitioned<E, A> {
return results.reduce(
({ errs, oks }: Partitioned<E, A>, result) =>
result.either(
(err) => ({ errs: [...errs, err], oks }),
(ok) => ({ errs, oks: [...oks, ok] }),
),
{ errs: [], oks: [] },
);
}
// ----- Methods
/**
* Applies a function to the value in an `Ok`, does nothing to an `Err`.
* Similar to {@link Optional.map}.
* @param f The function to apply if this is an `Ok`
* @returns {Result<E, B>} A new {@linkcode Result}
* @example
* const creditOne = Result.ok('Nicéphore Niépce');
* // Returns Ok('Photograph: Nicéphore Niépce')
* creditOne.map(name => `Photograph: ${name}`);
*
* const creditTwo = Result.err('No credit!');
* // Returns Err('No credit!')
* creditTwo.map(name => `Photograph: ${name}`);
*/
map<B>(f: (a: A) => B): Result<E, B> {
return this.flatMap((a) => Result.ok(f(a)));
}
}
class Ok<E, A> extends Result<E, A> {
/**
* The value wrapped inside an `Ok`. Can be extracted directly if
* {@linkcode Result.isOk|isOk} is used in a type guard, or using
* {@linkcode Result.either|either}.
*/
value: A;
flatMap<B>(f: (a: A) => Result<E, B>): Result<E, B> {
return f(this.value);
}
either<C>(_f: (e: E) => C, g: (a: A) => C): C {
return g(this.value);
}
mapError<F>(_g: (e: E) => F): Result<F, A> {
return Result.ok(this.value);
}
toOptional(): Optional<A> {
return Optional.some(this.value);
}
toOption(): Option<A> {
return some(this.value);
}
isOk(): this is Ok<E, A> {
return true;
}
isErr(): this is Err<E, A> {
return false;
}
constructor(a: A) {
super();
this.value = a;
}
}
class Err<E, A> extends Result<E, A> {
/**
* The error wrapped inside an `Err`. Can be extracted directly if
* {@linkcode Result.isErr|isErr} is used in a type guard, or using
* {@linkcode Result.either|either}.
*/
error: E;
flatMap<B>(_f: (a: A) => Result<E, B>): Result<E, B> {
return Result.err(this.error);
}
either<C>(f: (e: E) => C, _g: (a: A) => C): C {
return f(this.error);
}
mapError<F>(g: (e: E) => F): Result<F, A> {
return Result.err(g(this.error));
}
toOptional(): Optional<A> {
return Optional.none();
}
toOption(): Option<A> {
return none;
}
isOk(): this is Ok<E, A> {
return false;
}
isErr(): this is Err<E, A> {
return true;
}
constructor(e: E) {
super();
this.error = e;
}
}
// ----- Exports ----- //
export { Result };