Skip to content

Instantly share code, notes, and snippets.

@jtmueller
Last active June 12, 2024 15:16
Show Gist options
  • Save jtmueller/fa10732874d741db79e1f7883b0e0867 to your computer and use it in GitHub Desktop.
Save jtmueller/fa10732874d741db79e1f7883b0e0867 to your computer and use it in GitHub Desktop.
import { Err, Ok, type Result, getValues } from './result';
describe('Result', () => {
describe('Ok', () => {
test('should create an Ok result with the given value', () => {
const result = Ok('hello');
expect(result.ok).toBe(true);
expect(result.unwrap()).toBe('hello');
if (result.ok) {
expect(result.value).toBe('hello');
}
});
test('should have ResultOps methods', () => {
const result = Ok('hello');
expect(result.unwrap).toBeDefined();
expect(result.unwrapOr).toBeDefined();
expect(result.unwrapOrElse).toBeDefined();
expect(result.unwrapErr).toBeDefined();
expect(result.match).toBeDefined();
expect(result.map).toBeDefined();
expect(result.bind).toBeDefined();
expect(result.mapErr).toBeDefined();
expect(result.andThen).toBeDefined();
expect(result.orElse).toBeDefined();
});
describe('ResultOps (Ok)', () => {
const value = 'hello';
const result = Ok(value);
describe('unwrap', () => {
test('should return the value', () => {
expect(result.unwrap()).toBe(value);
});
});
describe('unwrapOr', () => {
test('should return the value', () => {
expect(result.unwrapOr('world')).toBe(value);
});
});
describe('unwrapOrElse', () => {
test('should return the value', () => {
const fn = vi.fn(() => 'world');
expect(result.unwrapOrElse(fn)).toBe(value);
expect(fn).not.toHaveBeenCalled();
});
});
describe('unwrapErr', () => {
test('should throw an error', () => {
expect(() => result.unwrapErr()).toThrowError();
});
});
describe('match', () => {
test('should call the Ok function with the value', () => {
const fn = vi.fn(() => 'world');
expect(result.match({ ok: fn, err: vi.fn() })).toBe('world');
expect(fn).toHaveBeenCalledWith(value);
});
});
describe('map', () => {
test('should return a new Ok with the mapped value', () => {
const fn = vi.fn((val: string) => val.toUpperCase());
const mapped = result.map(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe('HELLO');
expect(fn).toHaveBeenCalledWith(value);
});
});
describe('bind', () => {
test('should return a new Ok with the mapped value', () => {
const fn = vi.fn((val: string) => Ok(val.toUpperCase()));
const mapped = result.bind(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe('HELLO');
expect(fn).toHaveBeenCalledWith(value);
});
});
describe('mapErr', () => {
test('should return the same Ok result', () => {
const fn = vi.fn(() => new Error('Something went wrong'));
const mapped = result.mapErr(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe(value);
expect(fn).not.toHaveBeenCalled();
});
});
describe('andThen', () => {
test('should call the function with the value and return the result', () => {
const fn = vi.fn((val: string) => Ok(val.toUpperCase()));
const mapped = result.andThen(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe('HELLO');
expect(fn).toHaveBeenCalledWith(value);
});
});
describe('orElse', () => {
test('should return the same Ok result', () => {
const fn = vi.fn(() => Ok('world'));
const mapped = result.orElse(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe(value);
expect(fn).not.toHaveBeenCalled();
});
});
describe('iterable', () => {
test('Ok Result can be looped over', () => {
let val: string | undefined;
for (const v of result) {
val = v;
}
expect(val).toBe(value);
});
test('Ok Result can be converted into an array', () => {
expect([...result]).toEqual([value]);
});
test('can flatten nested results to get only success values', () => {
const results: Result<number, string>[] = [
Ok(1),
Ok(2),
Err('error'),
Ok(3),
Err('error'),
Err('error'),
Ok(4),
Ok(5),
Err('error'),
];
const values = getValues(results);
expect([...values]).toEqual([1, 2, 3, 4, 5]);
});
test('can merge multiple results into a single array of values', () => {
const res1 = Ok(10);
const res2 = Err('something went wrong');
const res3 = Ok(50);
const output = [...res1, ...res2, ...res3];
expect(output).toEqual([10, 50]);
});
});
});
});
describe('Err', () => {
test('should create an Err result with the given error', () => {
const error = new Error('Something went wrong');
const result = Err(error);
expect(result.ok).toBe(false);
expect(result.unwrapErr()).toBe(error);
if (!result.ok) {
expect(result.error).toBe(error);
}
});
test('should have ResultOps methods', () => {
const error = new Error('Something went wrong');
const result = Err(error);
expect(result.unwrap).toBeDefined();
expect(result.unwrapOr).toBeDefined();
expect(result.unwrapOrElse).toBeDefined();
expect(result.unwrapErr).toBeDefined();
expect(result.match).toBeDefined();
expect(result.map).toBeDefined();
expect(result.bind).toBeDefined();
expect(result.mapErr).toBeDefined();
expect(result.andThen).toBeDefined();
expect(result.orElse).toBeDefined();
});
describe('ResultOps (Err)', () => {
const error = new Error('Something went wrong');
const result = Err(error);
describe('unwrap', () => {
test('should throw an error', () => {
expect(() => result.unwrap()).toThrowError();
});
});
describe('unwrapOr', () => {
test('should return the default value', () => {
expect(result.unwrapOr('world')).toBe('world');
});
});
describe('unwrapOrElse', () => {
test('should call the function and return its result', () => {
const fn = vi.fn(() => 'world');
expect(result.unwrapOrElse(fn)).toBe('world');
expect(fn).toHaveBeenCalled();
});
});
describe('unwrapErr', () => {
test('should return the error', () => {
expect(result.unwrapErr()).toBe(error);
});
});
describe('match', () => {
test('should call the Err function with the error', () => {
const fn = vi.fn(() => 'world');
expect(result.match({ ok: vi.fn(), err: fn })).toBe('world');
expect(fn).toHaveBeenCalledWith(error);
});
});
describe('map', () => {
test('should return the same Err result', () => {
const fn = vi.fn((val: string) => val.toUpperCase());
const mapped = result.map(fn);
expect(mapped.ok).toBe(false);
expect(mapped.unwrapErr()).toBe(error);
expect(fn).not.toHaveBeenCalled();
});
});
describe('bind', () => {
test('should return Err when input is Err', () => {
const fn = vi.fn((val: string) => Ok(val.toUpperCase()));
const mapped = result.bind(fn);
expect(mapped.ok).toBe(false);
expect(mapped.unwrapErr()).toBe(error);
expect(fn).not.toHaveBeenCalled();
});
});
describe('mapErr', () => {
test('should return a new Err with the mapped error', () => {
const fn = vi.fn(() => new Error('Something else went wrong'));
const mapped = result.mapErr(fn);
expect(mapped.ok).toBe(false);
expect(mapped.unwrapErr().message).toBe('Something else went wrong');
expect(fn).toHaveBeenCalledWith(error);
});
});
describe('andThen', () => {
test('should return the same Err result', () => {
const fn = vi.fn(() => Err(new Error('Something else went wrong')));
const mapped = result.andThen(fn);
expect(mapped.ok).toBe(false);
expect(mapped.unwrapErr()).toBe(error);
expect(fn).not.toHaveBeenCalled();
});
});
describe('orElse', () => {
test('should call the function and return its result', () => {
const fn = vi.fn(() => Ok('world'));
const mapped = result.orElse(fn);
expect(mapped.ok).toBe(true);
expect(mapped.unwrap()).toBe('world');
expect(fn).toHaveBeenCalledWith(error);
});
});
describe('iterable', () => {
test('Err Result can be looped over', () => {
let val: string | undefined;
for (const v of result) {
val = v;
}
expect(val).toBeUndefined();
});
test('Err Result can be converted into an array', () => {
expect([...result]).toEqual([]);
});
});
});
});
});
/**
* Represents a successful result with a value of type T.
*
* @template T The type of the successful value.
*/
export interface Success<T, E> extends ResultOps<T, E> {
/** Indicates whether the result is successful. Always true for Success instances. */
readonly ok: true;
/** The success value. */
readonly value: T;
}
/**
* Represents a failure result with an error of type E.
*
* @template E The type of the error.
*/
export interface Failure<T, E> extends ResultOps<T, E> {
/** Indicates whether the result is successful. Always false for Failure instances. */
readonly ok: false;
/** The error value. */
readonly error: E;
}
/**
* Represents a result that can either be successful (`ok: true`) with a value of type `T`,
* or a failure (`ok: false`) with an error of type `E`.
*
* @template T The type of the successful value.
* @template E The type of the error.
*/
export type Result<T, E = unknown> = Success<T, E> | Failure<T, E>;
/**
* Creates a new `Ok` result with the given value.
*
* @typeparam T The type of the value contained in the `Ok` result.
* @typeparam E The type of the error contained in the `Err` result. Defaults to `any`.
* @param value The value to contain in the `Ok` result.
* @returns A new `Ok` result containing the given value.
*/
// biome-ignore lint/suspicious/noExplicitAny: needed here
export const Ok = function Ok<T, E = any>(value: T): Result<T, E> {
return new OkImpl(value);
};
/**
* Creates a new `Err` result containing the given error value.
*
* @typeparam E The type of the error value.
* @param error The error value to wrap in a `Result`.
* @returns A new `Err` result containing the given error value.
*/
// biome-ignore lint/suspicious/noExplicitAny: needed here
export const Err = function Err<E>(error: E): Result<any, E> {
return new ErrImpl(error);
};
/**
* A generator function that yields values from an iterable of Result objects,
* ignoring any error results and returning a single result containing all successful values.
* @template T The type of values in the Result objects.
* @param {Iterable<Result<T, any>>} results An iterable of Result objects.
* @returns {Iterable<T>} An iterable of the values contained in the Result objects for which the 'ok' property is true.
*/
export const getValues = function* getValues<T>(
// biome-ignore lint/suspicious/noExplicitAny: Needed for type compatibilty.
results: Iterable<Result<T, any>>,
): Iterable<T> {
for (const result of results) {
if (result.ok) {
yield result.value;
}
}
};
/**
* A Match contains a pair of functions that are called when a Result is matched against,
* one for successful matches and another for error matches. Either case must return the
* same type.
* @template T - The type of value if the match is successful.
* @template E - The type of error if the match is unsuccessful.
* @template U - The type returned by the `ok` or `err` function.
*/
export interface Match<T, E, U> {
/**
* A method that takes a value of type T and returns the result of applying the provided function to it if the match is successful (ok).
* @param val The value of type T to pass to the `ok` function.
* @returns {U} The result returned by the `ok` function.
*/
ok: (val: T) => U;
/**
* A method that takes a value of type E and returns the result of applying the provided function to it if the match is unsuccessful (err).
* @param val The value of type E to pass to the `err` function.
* @returns {U} The result returned by the `err` function.
*/
err: (val: E) => U;
}
/**
* A set of operations that can be performed on a `Result` object.
*/
export interface ResultOps<T, E> extends Iterable<T> {
/**
* Unwraps the `Ok` variant of the `Result`, returning the value inside.
* If the `Result` is an `Err`, this method will throw an error with the error message.
*/
unwrap(): T | never;
/**
* Unwraps the `Ok` variant of the `Result`, returning the value inside.
* If the `Result` is an `Err`, this method will return the provided default value.
* @param optb The default value to return if the `Result` is an `Err`.
*/
unwrapOr(optb: T): T;
/**
* Unwraps the `Ok` variant of the `Result`, returning the value inside.
* If the `Result` is an `Err`, this method will call the provided function with the error message and return its result.
* @param fn The function to call if the `Result` is an `Err`.
*/
unwrapOrElse(fn: (err: E) => T): T;
/**
* Unwraps the `Err` variant of the `Result`, returning the error message inside.
* If the `Result` is an `Ok`, this method will throw an error.
*/
unwrapErr(): E | never;
/**
* Calls the provided function with the value inside the `Ok` variant of the `Result` if it exists,
* or the error message inside the `Err` variant if it exists, and returns the result.
* @param fn The function to call with the value or error message.
*/
match<U>(fn: Match<T, E, U>): U;
/**
* Maps the value inside the `Ok` variant of the `Result` to a new value using the provided function.
* If the `Result` is an `Err`, this method will return an `Err` with the same error message.
* @param fn The function to map the value with.
*/
map<U>(fn: (val: T) => NonNullable<U>): Result<U, E>;
/**
* A function that takes the value of type T from a result and transforms it into a result containing a value of type U.
* @param fn The function to map the value with.
**/
bind<U>(fn: (val: T) => Result<U, E>): Result<U, E>;
/**
* Maps the error message inside the `Err` variant of the `Result` to a new error message using the provided function.
* If the `Result` is an `Ok`, this method will return an `Ok` with the same value.
* @param fn The function to map the error message with.
*/
mapErr<U>(fn: (err: E) => U): Result<T, U>;
/**
* Calls the provided function with the value inside the `Ok` variant of the `Result` if it exists,
* and returns the result of the function as a new `Result`.
* If the `Result` is an `Err`, this method will return an `Err` with the same error message.
* @param fn The function to call with the value.
*/
andThen<U>(fn: (val: T) => Result<U, E>): Result<U, E>;
/**
* Calls the provided function with the error message inside the `Err` variant of the `Result` if it exists,
* and returns the result of the function as a new `Result`.
* If the `Result` is an `Ok`, this method will return an `Ok` with the same value.
* @param fn The function to call with the error message.
*/
orElse<U>(fn: (err: E) => Result<U, E>): Result<T, E> | Result<U, E>;
}
class OkImpl<T> implements Success<T, never> {
readonly #value: T;
constructor(value: T) {
this.#value = value;
}
get value(): T {
return this.#value;
}
get ok(): true {
return true;
}
unwrap(): T {
return this.#value;
}
unwrapOr(_optb: T): T {
return this.#value;
}
unwrapOrElse(_fn: (err: never) => T): T {
return this.#value;
}
unwrapErr(): never {
throw new Error('called `Result.unwrapErr()` on an `Ok` value');
}
match<U>(fn: Match<T, never, U>): U {
return fn.ok(this.#value);
}
map<U>(fn: (val: T) => NonNullable<U>): Result<U, never> {
return Ok(fn(this.#value));
}
bind<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
return fn(this.#value);
}
mapErr(_fn: (err: never) => never): Result<T, never> {
return this;
}
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
return fn(this.#value);
}
orElse<U>(_fn: (err: never) => Result<U, never>): Result<T, never> {
return this;
}
*[Symbol.iterator](): Iterator<T> {
yield this.#value;
}
}
class ErrImpl<E> implements Failure<never, E> {
readonly #error: E;
constructor(error: E) {
this.#error = error;
}
get error(): E {
return this.#error;
}
get ok(): false {
return false;
}
unwrap(): never {
throw new Error(`Called 'Result.unwrap()' on an 'Err' value: ${this.#error}`);
}
unwrapOr<U>(optb: U): U {
return optb;
}
unwrapOrElse<U>(fn: (err: E) => U): U {
return fn(this.#error);
}
unwrapErr(): E {
return this.#error;
}
match<U>(fn: Match<never, E, U>): U {
return fn.err(this.#error);
}
map<U>(_fn: (val: never) => never): Result<U, E> {
return this;
}
bind<U>(_fn: (val: never) => Result<U, E>): Result<U, E> {
return this;
}
mapErr<U>(fn: (err: E) => U): Result<never, U> {
return new ErrImpl(fn(this.#error));
}
andThen<U>(_fn: (val: never) => Result<U, E>): Result<U, E> {
return this;
}
orElse<U>(fn: (err: E) => Result<U, E>): Result<U, E> {
return fn(this.#error);
}
*[Symbol.iterator](): Iterator<never> {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment