Skip to content

Instantly share code, notes, and snippets.

@OliverBrotchie
Created July 18, 2022 12:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverBrotchie/4bc8168e02e56a84210667381f135b8d to your computer and use it in GitHub Desktop.
Save OliverBrotchie/4bc8168e02e56a84210667381f135b8d to your computer and use it in GitHub Desktop.
A L/R Result class
function divide(left: number, right: number): Result<number, Error> {
if (right === 0) return Err("Divided by zero");
return Ok(left / right);
}
class DivisionError extends Error {
constructor(message: string) {
super(message);
}
}
function typedDivide(
left: number,
right: number
): Result<number, DivisionError> {
if (right === 0) return Err(new DivisionError("Divided by zero"));
return Ok(left / right);
}
// Get the contained Ok value or bubble an error with the provided message.
const unwrappedResult = divide(2, 0).expect("Divided by zero");
// Gets the contained Ok value or returns a provided default.
const result = divide(2, 0).unwrapOr(0);
// Map one type of Error to another
const mapedErrorResult = divide(2, 0).mapErr((e) => new TestError(e.message));
/**
* A Rust-like Result class.
*
* Note: Please use either Ok or Err to construct Results.
*
* @example
* ```
* function divide(left: number, right: number): Result<number, Error> {
* if (right === 0) return Err("Divided by zero");
*
* return Ok(left / right);
* }
*
* ```
*/
export class Result<T, E extends Error> {
private val: T | E;
/**
* A constructor for a Result
*
* Note: Please use either Ok or Err to construct Results.
* @param input The value to wrap in a Result.
*/
constructor(input: T | E) {
this.val = input;
}
/**
* Returns true if contained value isnt an error.
*/
isOk(): boolean {
return !(this.val instanceof Error || Error.isPrototypeOf(this.val));
}
/**
* Returns true if contained value is an error.
*/
isErr(): boolean {
return this.val instanceof Error || Error.isPrototypeOf(this.val);
}
/**
* Returns the contained Ok value, consuming the self value.
* @throws An Error with a given message if contained value is not Ok.
*
* @param msg An error message to throw if contained value is an Error.
*/
expect(msg: string): T {
if (this.isErr()) {
throw new Error(msg);
}
return this.val as T;
}
/**
* Returns the contained Ok value, consuming the self value.
* @throws An Error if contained value is not Ok.
*/
unwrap(): T {
if (this.isErr()) {
throw new Error(
`Unwrap called ${(this.val as E).name} - ${(this.val as E).message}${
(this.val as E)?.stack ? ", " + ((this.val as E).stack as string) : ""
}`
);
}
return this.val as T;
}
/**
* Returns the contained Error value, consuming the self value.
* @throws An Error if contained value is not an Error.
*/
unwrapErr(): E {
if (this.isOk()) {
throw new Error(
`UnwrapError called on value - ${this.val as unknown as string}`
);
}
return this.val as E;
}
/**
* Returns the contained Ok value or a provided default.
*
* @param fallback A default value to return if contained value is an Error.
*/
unwrapOr(fallback: T): T {
if (this.isErr()) {
return fallback;
}
return this.val as T;
}
/**
* Returns the contained Ok value or computes it from a closure.
*
* @param fn A function that computes a new value.
*/
unwrapOrElse(fn: (input: E) => T): T {
if (this.isErr()) {
return fn(this.val as E);
}
return this.val as T;
}
/**
* Maps a Result<T, E> to Result<U, E> by applying a function to a contained Ok value, leaving an Error value untouched.
*
* @param fn A mapping function.
*/
map<U>(fn: (input: T) => U): Result<U, E> {
if (this.isOk()) {
return new Result<U, E>(fn(this.val as T));
}
return this as unknown as Result<U, E>;
}
/**
* Maps a Result<T, E> to Result<T, U> by applying a function to a contained Error value, leaving an Ok value untouched.
*
* @param fn A mapping function.
*/
mapErr<U extends Error>(fn: (input: E) => U): Result<T, U> {
if (this.isOk()) {
return this as unknown as Result<T, U>;
}
return new Result<T, U>(fn(this.val as E));
}
/**
* Returns the provided fallback (if Error), or applies a function to the contained value.
*
* @param fallback A defualt value
* @param fn A mapping function.
*/
mapOr<U>(fallback: U, fn: (input: T) => U): U {
if (this.isOk()) {
return fn(this.val as T);
}
return fallback;
}
/**
* Returns `or` if the result is Error, otherwise returns self.
*
* @param or A alternative Result value
*/
or(or: Result<T, E>): Result<T, E> {
if (this.isOk()) {
return this;
}
return or;
}
/**
* Returns contained value for use in matching.
*
* Note: Please only use this to match against in `if` or `swtich` statments.
*
* @example
* ```
* function coolOrNice(input: Result<string, Error>): Result<void, Error> {
* switch (input.peek()) {
* case "cool":
* console.log("Input was the coolest!");
* break;
* case "nice":
* console.log("Input was was the nicest!");
* break
* default:
* return Err("Input neither cool nor nice.");
* }
* return Ok()
* }
* ```
*/
peek(): T | E {
return this.val;
}
/**
* Throws contained Errors, consuming the Result
* @throws The contained Error if contained value is an Error.
*/
throw(): void {
if (this.isErr()) {
throw this.val;
}
}
}
/**
* Return a non-error value result.
*
* @param input a value that does not extend the `Error` type.
* @example
* ```
* function divide(left: number, right: number): Result<number, Error> {
* if (right === 0) return Err("Divided by zero");
*
* return Ok(left / right);
* }
*
* ```
*/
export function Ok<T, E extends Error>(input?: Exclude<T, E>) {
return new Result<T, E>(input as T);
}
/**
* Return a error result.
*
* @param input a value that extends the `Error` type.
* @example
* ```
* function divide(left: number, right: number): Result<number, Error> {
* if (right === 0) return Err("Divided by zero");
*
* return Ok(left / right);
* }
*
* ```
*/
export function Err<T, E extends Error>(input: string | E): Result<T, E> {
if (typeof input === "string") {
return new Result<T, Error>(new Error(input)) as Result<T, E>;
}
return new Result<T, E>(input);
}
/**
* Partition an array of Results into Ok values and Errors
*
* @param input An array of Results
* @example
* ```
* const results = [Ok(2),Ok(16),Err("Something went wrong!")]
*
* partition(results) // {ok:[2, 16], err:[Error("Something went wrong!")]}
*
* ```
*/
export function partition<T, E extends Error>(
input: Array<Result<T, E>>
): { ok: Array<T>; err: Array<E> } {
return input.reduce(
(acc: { ok: Array<T>; err: Array<E> }, e) => {
if (e.isOk()) acc.ok.push(e.unwrap());
else acc.err.push(e.unwrapErr());
return acc;
},
{
ok: [],
err: [],
}
);
}
/**
* Run a closure in a `try`/`catch` and convert it into a Result
* @param fn The closure to run
* @returns The Result of the closure
*/
export async function from<T>(
fn: (() => T) | (() => Promise<T>)
): Promise<Result<T, Error>> {
try {
return new Result<T, Error>(await fn());
} catch (e: unknown) {
return new Result<T, Error>(e as Error);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment