Last active
June 12, 2024 15:16
-
-
Save jtmueller/fa10732874d741db79e1f7883b0e0867 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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([]); | |
}); | |
}); | |
}); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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