Created
June 26, 2025 11:20
-
-
Save kukalajet/6d517bb03acfad575a1a1373479e5d25 to your computer and use it in GitHub Desktop.
Result
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 { describe, expect, test, vi } from 'vitest'; | |
import { | |
all, | |
combine, | |
err, | |
fromPromise, | |
fromThrowable, | |
ok, | |
type Result, | |
} from './result'; | |
describe('Factories and Type Guards', () => { | |
test('ok(value) should create a successful result', () => { | |
const success = ok(42); | |
expect(success.isOk()).toBe(true); | |
expect(success.isErr()).toBe(false); | |
// Safely access value after type guard | |
if (success.isOk()) { | |
expect(success.value).toBe(42); | |
} else { | |
throw new Error('isOk() should have returned true'); | |
} | |
}); | |
test('err(error) should create a failure result', () => { | |
const failure = err('Something went wrong'); | |
expect(failure.isOk()).toBe(false); | |
expect(failure.isErr()).toBe(true); | |
// Safely access error after type guard | |
if (failure.isErr()) { | |
expect(failure.error).toBe('Something went wrong'); | |
} else { | |
throw new Error('isErr() should have returned true'); | |
} | |
}); | |
}); | |
describe('map', () => { | |
test('should apply a function to an Ok value', () => { | |
const success = ok(5); | |
const result = success.map((x) => x * 2); | |
expect(result.isOk()).toBe(true); | |
result.match( | |
(value) => expect(value).toBe(10), | |
() => { | |
throw new Error('Expected Ok result'); | |
}, | |
); | |
}); | |
test('should not apply a function to an Err value', () => { | |
const failure: Result<number, string> = err('error'); | |
const result = failure.map((x) => x * 2); | |
expect(result.isErr()).toBe(true); | |
// The instance should be the same | |
result.match( | |
() => { | |
throw new Error('Should not have been a success'); | |
}, | |
(error) => expect(error).toBe('error'), | |
); | |
}); | |
}); | |
describe('mapErr', () => { | |
test('should not apply a function to an Ok value', () => { | |
const success: Result<number, string> = ok(100); | |
const result = success.mapErr((err) => `New Error: ${err}`); | |
expect(result.isOk()).toBe(true); | |
// The instance should be the same | |
expect(result).toBe(success); | |
result.match( | |
(value) => expect(value).toBe(100), | |
() => { | |
throw new Error('Should not have been an error'); | |
}, | |
); | |
}); | |
test('should apply a function to an Err value', () => { | |
const failure = err('original error'); | |
const result = failure.mapErr((err) => `mapped: ${err}`); | |
expect(result.isErr()).toBe(true); | |
result.match( | |
() => { | |
throw new Error('Should not have been a success'); | |
}, | |
(error) => expect(error).toBe('mapped: original error'), | |
); | |
}); | |
}); | |
describe('andThen', () => { | |
const successfulChain = (x: number): Result<number, string> => ok(x + 1); | |
const failingChain = (x: number): Result<number, string> => | |
err(`fail on ${x}`); | |
test('should chain a successful operation on an Ok value', () => { | |
const success = ok<number, string>(10); | |
const result = success.andThen(successfulChain); | |
expect(result.isOk()).toBe(true); | |
result.match( | |
(value) => expect(value).toBe(11), | |
() => { | |
throw new Error('Should not have been an error'); | |
}, | |
); | |
}); | |
test('should chain a failing operation on an Ok value', () => { | |
const success = ok<number, string>(10); | |
const result = success.andThen(failingChain); | |
expect(result.isErr()).toBe(true); | |
result.match( | |
() => { | |
throw new Error('Should not have been a success'); | |
}, | |
(error) => expect(error).toBe('fail on 10'), | |
); | |
}); | |
test('should short-circuit and not execute the function on an Err value', () => { | |
const failure: Result<number, string> = err('initial error'); | |
const result = failure.andThen(successfulChain); | |
expect(result.isErr()).toBe(true); | |
// The instance should be the same | |
expect(result).toBe(failure); | |
result.match( | |
() => { | |
throw new Error('Should not have been a success'); | |
}, | |
(error) => expect(error).toBe('initial error'), | |
); | |
}); | |
}); | |
describe('match', () => { | |
const onOk = (value: string) => `Success: ${value}`; | |
const onErr = (error: number) => `Error code: ${error}`; | |
test('should execute the okFn on an Ok value', () => { | |
const success = ok('data'); | |
const result = success.match(onOk, onErr); | |
expect(result).toBe('Success: data'); | |
}); | |
test('should execute the errFn on an Err value', () => { | |
const failure = err(404); | |
const result = failure.match(onOk, onErr); | |
expect(result).toBe('Error code: 404'); | |
}); | |
}); | |
describe('unwrapOr', () => { | |
test('should return the contained value for an Ok', () => { | |
const success = ok(100); | |
const value = success.unwrapOr(999); | |
expect(value).toBe(100); | |
}); | |
test('should return the default value for an Err', () => { | |
const failure: Result<number, string> = err('error'); | |
const value = failure.unwrapOr(999); | |
expect(value).toBe(999); | |
}); | |
}); | |
describe('unwrapOrElse', () => { | |
test('should return the contained value for an Ok', () => { | |
const success = ok(100); | |
const value = success.unwrapOrElse(() => 999); | |
expect(value).toBe(100); | |
}); | |
test('should compute the value from a function for an Err', () => { | |
const failure: Result<number, string> = err('error'); | |
const value = failure.unwrapOrElse((e) => e.length); | |
expect(value).toBe(5); | |
}); | |
}); | |
describe('unwrap and expect', () => { | |
test('unwrap should return the value for an Ok', () => { | |
const success = ok('hello'); | |
expect(success.unwrap()).toBe('hello'); | |
}); | |
test('unwrap should throw an error for an Err', () => { | |
const failure: Result<string, string> = err('boom'); | |
expect(() => failure.unwrap()).toThrow( | |
'Called `unwrap()` on an `Err` value: boom', | |
); | |
}); | |
test('expect should return the value for an Ok', () => { | |
const success = ok('hello'); | |
expect(success.expect('should not throw')).toBe('hello'); | |
}); | |
test('expect should throw an error with a custom message for an Err', () => { | |
const failure: Result<string, string> = err('boom'); | |
expect(() => failure.expect('Testing expect')).toThrow( | |
'Testing expect: boom', | |
); | |
}); | |
}); | |
describe('or and orElse', () => { | |
const fallback = ok<string, number>('fallback'); | |
const lazyFallback = () => ok<string, number>('lazy fallback'); | |
test('or should return self for an Ok', () => { | |
const success = ok<string, number>('success'); | |
expect(success.or(fallback)).toBe(success); | |
}); | |
test('or should return the provided result for an Err', () => { | |
const failure = err<string, number>(404); | |
expect(failure.or(fallback)).toBe(fallback); | |
}); | |
test('orElse should return self for an Ok', () => { | |
const success = ok<string, number>('success'); | |
expect(success.orElse(lazyFallback)).toBe(success); | |
}); | |
test('orElse should call the function and return its result for an Err', () => { | |
const failure = err<string, number>(404); | |
const result = failure.orElse(lazyFallback); | |
expect(result.isOk()).toBe(true); | |
expect(result.unwrap()).toBe('lazy fallback'); | |
}); | |
}); | |
describe('tap and tapErr', () => { | |
test('tap should call the function for an Ok', () => { | |
const spy = vi.fn(); | |
const success = ok(10); | |
success.tap(spy); | |
expect(spy).toHaveBeenCalledWith(10); | |
}); | |
test('tap should not call the function for an Err', () => { | |
const spy = vi.fn(); | |
const failure = err('error'); | |
failure.tap(spy); | |
expect(spy).not.toHaveBeenCalled(); | |
}); | |
test('tapErr should not call the function for an Ok', () => { | |
const spy = vi.fn(); | |
const success = ok(10); | |
success.tapErr(spy); | |
expect(spy).not.toHaveBeenCalled(); | |
}); | |
test('tapErr should call the function for an Err', () => { | |
const spy = vi.fn(); | |
const failure = err('error'); | |
failure.tapErr(spy); | |
expect(spy).toHaveBeenCalledWith('error'); | |
}); | |
}); | |
describe('fromPromise', () => { | |
test('should resolve to an Ok when the promise resolves', async () => { | |
const resolvingPromise = Promise.resolve('yay!'); | |
const resultPromise = fromPromise( | |
resolvingPromise, | |
() => new Error('Should not be called'), | |
); | |
// Test using resolves matcher | |
await expect(resultPromise).resolves.toEqual(ok('yay!')); | |
// Also test by awaiting directly | |
const result = await resultPromise; | |
expect(result.isOk()).toBe(true); | |
result.match( | |
(val) => expect(val).toBe('yay!'), | |
() => { | |
throw new Error('Promise should have resolved'); | |
}, | |
); | |
}); | |
test('should resolve to an Err when the promise rejects', async () => { | |
const rejectingPromise = Promise.reject('oops'); | |
const errorMapper = (e: unknown) => `Caught: ${e}`; | |
const resultPromise = fromPromise(rejectingPromise, errorMapper); | |
// Test using resolves matcher | |
await expect(resultPromise).resolves.toEqual(err('Caught: oops')); | |
// Also test by awaiting directly | |
const result = await resultPromise; | |
expect(result.isErr()).toBe(true); | |
result.match( | |
() => { | |
throw new Error('Promise should have rejected'); | |
}, | |
(e) => expect(e).toBe('Caught: oops'), | |
); | |
}); | |
}); | |
describe('fromThrowable', () => { | |
test('should return an Ok when the function returns a value', () => { | |
const result = fromThrowable( | |
() => 42, | |
() => 'error', | |
); | |
expect(result).toEqual(ok(42)); | |
}); | |
test('should return an Err when the function throws', () => { | |
const error = new Error('boom'); | |
const result = fromThrowable( | |
() => { | |
throw error; | |
}, | |
(e) => e as Error, | |
); | |
expect(result).toEqual(err(error)); | |
}); | |
test('should correctly parse JSON', () => { | |
const parse = (json: string) => | |
fromThrowable( | |
() => JSON.parse(json), | |
(e) => e as Error, | |
); | |
expect(parse('{"a":1}')).toEqual(ok({ a: 1 })); | |
expect(parse('{a:1}').isErr()).toBe(true); | |
}); | |
}); | |
describe('Symbol.iterator', () => { | |
test('should be iterable for an Ok', () => { | |
const success = ok(10); | |
const values = []; | |
for (const value of success) { | |
values.push(value); | |
} | |
expect(values).toEqual([10]); | |
}); | |
test('should not iterate for an Err', () => { | |
const failure: Result<number, string> = err('error'); | |
const values = []; | |
for (const value of failure) { | |
values.push(value); | |
} | |
expect(values).toEqual([]); | |
}); | |
}); | |
describe('toJSON', () => { | |
test('should serialize Ok correctly', () => { | |
const success = ok({ data: 'value' }); | |
expect(JSON.stringify(success)).toBe( | |
'{"status":"ok","value":{"data":"value"}}', | |
); | |
}); | |
test('should serialize Err correctly', () => { | |
const failure = err('error message'); | |
expect(JSON.stringify(failure)).toBe( | |
'{"status":"err","error":"error message"}', | |
); | |
}); | |
}); | |
describe('Symbol.toStringTag', () => { | |
test('should return a custom string tag for Ok', () => { | |
const success = ok(42); | |
expect(Object.prototype.toString.call(success)).toBe('[object Ok(42)]'); | |
}); | |
test('should return a custom string tag for Err', () => { | |
const failure = err('oops'); | |
expect(Object.prototype.toString.call(failure)).toBe('[object Err(oops)]'); | |
}); | |
}); | |
describe('combine', () => { | |
test('should return an Ok tuple when all results are Ok', () => { | |
const result = combine([ok(1), ok('hello'), ok(true)]); | |
expect(result.isOk()).toBe(true); | |
result.match( | |
(value) => expect(value).toEqual([1, 'hello', true]), | |
() => { | |
throw new Error('Should be Ok'); | |
}, | |
); | |
}); | |
test('should return the first Err when one result is an Err', () => { | |
const result = combine([ok(1), err('error'), ok(true)]); | |
expect(result.isErr()).toBe(true); | |
result.match( | |
() => { | |
throw new Error('Should be Err'); | |
}, | |
(error) => expect(error).toBe('error'), | |
); | |
}); | |
test('should handle mixed error types', () => { | |
const result = combine([ok(1), err(404), err('oops')]); | |
expect(result.isErr()).toBe(true); | |
result.match( | |
() => { | |
throw new Error('Should be Err'); | |
}, | |
(error) => expect(error).toBe(404), | |
); | |
}); | |
test('should handle an empty array', () => { | |
const result = combine([]); | |
expect(result).toEqual(ok([])); | |
}); | |
}); | |
describe('all', () => { | |
test('should return an Ok array when all results are Ok', () => { | |
const result = all([ok(1), ok(2), ok(3)]); | |
expect(result).toEqual(ok([1, 2, 3])); | |
}); | |
test('should return the first Err when one result is an Err', () => { | |
const result = all([ok(1), err('error'), ok(3)]); | |
expect(result).toEqual(err('error')); | |
}); | |
test('should return an empty Ok for an empty array', () => { | |
const result = all([]); | |
expect(result).toEqual(ok([])); | |
}); | |
}); |
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
/* eslint-disable no-unused-vars */ | |
/** | |
* A class representing a successful outcome. | |
* @template T The type of the success value. | |
* @template E The type of the error value. | |
*/ | |
export class Ok<T, E> { | |
readonly #value: T; | |
constructor(value: T) { | |
this.#value = value; | |
} | |
/** The encapsulated value. */ | |
get value(): T { | |
return this.#value; | |
} | |
/** | |
* Returns `true` if the result is `Ok`. | |
*/ | |
isOk(): this is Ok<T, E> { | |
return true; | |
} | |
/** | |
* Returns `false` if the result is `Ok`. | |
*/ | |
isErr(): this is Err<T, E> { | |
return false; | |
} | |
/** | |
* Maps a `Result<T, E>` to `Result<U, E>` by applying a function to a | |
* contained `Ok` value, leaving an `Err` value untouched. | |
* | |
* @template U The new success type. | |
* @param {(value: T) => U} fn The function to apply to the `Ok` value. | |
* @returns {Ok<U, E>} A new `Ok` with the transformed value. | |
* @example | |
* const result = ok(5); | |
* const doubled = result.map((x) => x * 2); // Ok(10) | |
*/ | |
map<U>(fn: (value: T) => U): Ok<U, E> { | |
return new Ok(fn(this.#value)); | |
} | |
/** | |
* Maps a `Result<T, E>` to `Result<T, F>` by applying a function to a | |
* contained `Err` value, leaving an `Ok` value untouched. | |
* This function can be used to pass through a successful value while handling a potential error. | |
* | |
* @template F The new error type. | |
* @param {(error: E) => F} _fn The function to apply to an error. | |
* @returns {Ok<T, F>} The original `Ok` value, with the error type parameter changed. | |
* @example | |
* const success = ok(10); | |
* const result = success.mapErr((err) => new Error(err)); // ok(10) | |
*/ | |
mapErr<F>(_fn: (error: E) => F): Ok<T, F> { | |
return this as unknown as Ok<T, F>; | |
} | |
/** | |
* Returns the provided `Result` `res` if the result is `Err`, | |
* otherwise returns the `Ok` value of `this`. | |
* | |
* @param {Result<T, E>} _res The `Result` to return if `this` is an `Err`. | |
* @returns {Ok<T, E>} The original `Ok` instance. | |
* @example | |
* const success = ok(10); | |
* const fallback = ok(0); | |
* const result = success.or(fallback); // ok(10) | |
*/ | |
or(_res: Result<T, E>): Ok<T, E> { | |
return this; | |
} | |
/** | |
* Calls the provided function `fn` if the result is `Err`, | |
* otherwise returns the `Ok` value of `this`. | |
* | |
* @param {(error: E) => Result<T, E>} _fn The function to call if `this` is an `Err`. | |
* @returns {Ok<T, E>} The original `Ok` instance. | |
* @example | |
* const success = ok(10); | |
* const result = success.orElse(() => ok(0)); // ok(10) | |
*/ | |
orElse(_fn: (error: E) => Result<T, E>): Ok<T, E> { | |
return this; | |
} | |
/** | |
* Chains a function that returns a `Result` to the current `Ok` value. | |
* Also known as `flatMap` or `bind`. | |
* | |
* @template U The new success type for the chained operation. | |
* @param {(value: T) => Result<U, E>} fn The function to call with the `Ok` value. | |
* @returns {Result<U, E>} The result of the chained function. | |
* @example | |
* const divide = (x: number) => (x === 0 ? err('Division by zero') : ok(10 / x)); | |
* const result = ok(2).andThen(divide); // Ok(5) | |
* const errorResult = ok(0).andThen(divide); // Err("Division by zero") | |
*/ | |
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> { | |
return fn(this.#value); | |
} | |
/** | |
* Unwraps the `Result`, returning the contained `Ok` value. | |
* Throws an error if the result is an `Err`. | |
* | |
* @returns {T} The `Ok` value. | |
* @example | |
* const success = ok(10); | |
* const value = success.unwrap(); // 10 | |
*/ | |
unwrap(): T { | |
return this.#value; | |
} | |
/** | |
* Unwraps the `Result`, returning the contained `Ok` value or a provided default. | |
* | |
* @param {U} _defaultValue The default value. | |
* @returns {T} The `Ok` value. | |
* @example | |
* const success = ok(10); | |
* const value = success.unwrapOr(0); // 10 | |
*/ | |
unwrapOr<U>(_defaultValue: U): T { | |
return this.#value; | |
} | |
/** | |
* Unwraps the `Result`, returning the `Ok` value or computing it from a function. | |
* | |
* @param {(error: E) => T} _fn The function to generate a default value from an error. | |
* @returns {T} The `Ok` value. | |
* @example | |
* const success = ok(10); | |
* const value = success.unwrapOrElse(() => 0); // 10 | |
*/ | |
unwrapOrElse(_fn: (error: E) => T): T { | |
return this.#value; | |
} | |
/** | |
* Unwraps the `Result`, returning the `Ok` value. | |
* Throws an error with a custom message if the result is an `Err`. | |
* | |
* @param {string} _message The custom error message. | |
* @returns {T} The `Ok` value. | |
* @example | |
* const success = ok(10); | |
* const value = success.expect('should not fail'); // 10 | |
*/ | |
expect(_message: string): T { | |
return this.#value; | |
} | |
/** | |
* "Unwraps" the `Result` by providing functions to handle both the `Ok` and `Err` cases. | |
* | |
* @param {(value: T) => A} okFn The function to call if the result is `Ok`. | |
* @param {(error: E) => B} _errFn The function to call if the result is `Err`. | |
* @returns {A} The result of calling `okFn`. | |
* @example | |
* const result = ok(42); | |
* const message = result.match( | |
* (value) => `Success: ${value}`, | |
* (error) => `Error: ${error}`, | |
* ); // "Success: 42" | |
*/ | |
match<A, B>(okFn: (value: T) => A, _errFn: (error: E) => B): A { | |
return okFn(this.#value); | |
} | |
/** | |
* Performs a side-effect with the `Ok` value, returning the original `Result`. | |
* | |
* @param {(value: T) => void} fn The function to call with the `Ok` value. | |
* @returns {Ok<T, E>} The original `Ok` instance. | |
* @example | |
* ok(10).tap(console.log); // logs 10 | |
*/ | |
tap(fn: (value: T) => void): Ok<T, E> { | |
fn(this.#value); | |
return this; | |
} | |
/** | |
* Performs a side-effect with the `Err` value, returning the original `Result`. | |
* Does nothing for an `Ok`. | |
* | |
* @param {(error: E) => void} _fn The function to call with the `Err` value. | |
* @returns {Ok<T, E>} The original `Ok` instance. | |
* @example | |
* ok(10).tapErr(console.error); // does not log | |
*/ | |
tapErr(_fn: (error: E) => void): Ok<T, E> { | |
return this; | |
} | |
/** | |
* Allows the `Result` to be used in a `for...of` loop. | |
* Yields the `Ok` value once. | |
* @example | |
* const success = ok(10); | |
* for (const value of success) { | |
* console.log(value); // logs 10 | |
* } | |
*/ | |
*[Symbol.iterator](): Iterator<T> { | |
yield this.#value; | |
} | |
/** | |
* Custom `toJSON` implementation for `Ok`. | |
* @example | |
* const success = ok(10); | |
* JSON.stringify(success); // '{"status":"ok","value":10}' | |
*/ | |
toJSON(): { status: 'ok'; value: T } { | |
return { status: 'ok', value: this.#value }; | |
} | |
/** | |
* Returns a string representation of the `Ok` object. | |
* @example | |
* const success = ok(10); | |
* Object.prototype.toString.call(success); // '[object Ok(10)]' | |
*/ | |
get [Symbol.toStringTag](): string { | |
return `Ok(${this.#value})`; | |
} | |
} | |
/** | |
* A class representing a failed outcome. | |
* @template T The type of the success value. | |
* @template E The type of the error value. | |
*/ | |
export class Err<T, E> { | |
readonly #error: E; | |
constructor(error: E) { | |
this.#error = error; | |
} | |
/** The encapsulated error. */ | |
get error(): E { | |
return this.#error; | |
} | |
/** | |
* Returns `false` if the result is `Err`. | |
*/ | |
isOk(): this is Ok<T, E> { | |
return false; | |
} | |
/** | |
* Returns `true` if the result is `Err`. | |
*/ | |
isErr(): this is Err<T, E> { | |
return true; | |
} | |
/** | |
* Maps a `Result<T, E>` to `Result<U, E>` by applying a function to a | |
* contained `Ok` value, leaving an `Err` value untouched. | |
* | |
* @template U The new success type. | |
* @param {(value: T) => U} _fn The function to apply to an `Ok` value. | |
* @returns {Err<U, E>} The original `Err` instance, with the success type parameter changed. | |
* @example | |
* const result = err('404'); | |
* const doubled = result.map((x) => x * 2); // Err("404") - unchanged | |
*/ | |
map<U>(_fn: (value: T) => U): Err<U, E> { | |
return this as unknown as Err<U, E>; | |
} | |
/** | |
* Maps a `Result<T, E>` to `Result<T, F>` by applying a function to a | |
* contained `Err` value, leaving an `Ok` value untouched. | |
* | |
* @template F The new error type. | |
* @param {(error: E) => F} fn The function to apply to the `Err` value. | |
* @returns {Err<T, F>} A new `Err` with the transformed error. | |
* @example | |
* const result = err('404'); | |
* const errorWithCode = result.mapErr((msg) => ({ code: 404, message: msg })); | |
* // Err({ code: 404, message: "404" }) | |
*/ | |
mapErr<F>(fn: (error: E) => F): Err<T, F> { | |
return new Err(fn(this.#error)); | |
} | |
/** | |
* Returns the provided `Result` `res` if the result is `Err`. | |
* | |
* @param {Result<T, E>} res The `Result` to return. | |
* @returns {Result<T, E>} The provided `Result`. | |
* @example | |
* const failure = err('error'); | |
* const fallback = ok(0); | |
* const result = failure.or(fallback); // ok(0) | |
*/ | |
or(res: Result<T, E>): Result<T, E> { | |
return res; | |
} | |
/** | |
* Calls the provided function `fn` if the result is `Err`. | |
* | |
* @param {(error: E) => Result<T, E>} fn The function to call. | |
* @returns {Result<T, E>} The `Result` returned by `fn`. | |
* @example | |
* const failure = err('error'); | |
* const result = failure.orElse(() => ok(0)); // ok(0) | |
*/ | |
orElse(fn: (error: E) => Result<T, E>): Result<T, E> { | |
return fn(this.#error); | |
} | |
/** | |
* Chains a function that returns a `Result`. Does nothing for an `Err`. | |
* | |
* @template U The new success type for the chained operation. | |
* @param {(value: T) => Result<U, E>} _fn The function to call with the value. | |
* @returns {Err<U, E>} This `Err` instance, but with a new success type. | |
* @example | |
* const divide = (x: number) => (x === 0 ? err('Division by zero') : ok(10 / x)); | |
* const result = err('Invalid input').andThen(divide); // Err("Invalid input") - unchanged | |
*/ | |
andThen<U>(_fn: (value: T) => Result<U, E>): Err<U, E> { | |
return this as unknown as Err<U, E>; | |
} | |
/** | |
* Unwraps the `Result`, throwing an error because it is an `Err`. | |
* | |
* @throws {Error} Always throws. | |
* @example | |
* const failure = err('error'); | |
* // throws Error: Called `unwrap()` on an `Err` value: error | |
* failure.unwrap(); | |
*/ | |
unwrap(): T { | |
throw new Error(`Called \`unwrap()\` on an \`Err\` value: ${this.#error}`); | |
} | |
/** | |
* Unwraps the `Result`, returning a provided default value. | |
* | |
* @template U The type of the default value. | |
* @param {U} defaultValue The default value. | |
* @returns {U} The default value. | |
* @example | |
* const failure = err('error'); | |
* const value = failure.unwrapOr(0); // 0 | |
*/ | |
unwrapOr<U>(defaultValue: U): U { | |
return defaultValue; | |
} | |
/** | |
* Unwraps the `Result`, computing a default value from the `Err`. | |
* | |
* @param {(error: E) => T} fn The function to generate a default value. | |
* @returns {T} The computed default value. | |
* @example | |
* const failure = err('error'); | |
* const value = failure.unwrapOrElse((e) => e.length); // 5 | |
*/ | |
unwrapOrElse(fn: (error: E) => T): T { | |
return fn(this.#error); | |
} | |
/** | |
* Unwraps the `Result`, throwing an error with a custom message. | |
* | |
* @param {string} message The custom error message. | |
* @throws {Error} Always throws with the custom message. | |
* @example | |
* const failure = err('error'); | |
* // throws Error: My custom message: error | |
* failure.expect('My custom message'); | |
*/ | |
expect(message: string): T { | |
throw new Error(`${message}: ${this.#error}`); | |
} | |
/** | |
* "Unwraps" the `Result` by providing functions to handle both the `Ok` and `Err` cases. | |
* | |
* @param {(value: T) => A} _okFn The function to call if the result is `Ok`. | |
* @param {(error: E) => B} errFn The function to call if the result is `Err`. | |
* @returns {B} The result of calling `errFn`. | |
* @example | |
* const result = err('Not found'); | |
* const message = result.match( | |
* (value) => `Success: ${value}`, | |
* (error) => `Error: ${error}`, | |
* ); // "Error: Not found" | |
*/ | |
match<A, B>(_okFn: (value: T) => A, errFn: (error: E) => B): B { | |
return errFn(this.#error); | |
} | |
/** | |
* Performs a side-effect with the `Ok` value, returning the original `Result`. | |
* Does nothing for an `Err`. | |
* | |
* @param {(value: T) => void} _fn The function to call with the `Ok` value. | |
* @returns {Err<T, E>} The original `Err` instance. | |
* @example | |
* const failure = err('error'); | |
* failure.tap(console.log); // does not log | |
*/ | |
tap(_fn: (value: T) => void): Err<T, E> { | |
return this; | |
} | |
/** | |
* Performs a side-effect with the `Err` value, returning the original `Result`. | |
* | |
* @param {(error: E) => void} fn The function to call with the `Err` value. | |
* @returns {Err<T, E>} The original `Err` instance. | |
* @example | |
* const failure = err('error'); | |
* failure.tapErr(console.error); // logs "error" | |
*/ | |
tapErr(fn: (error: E) => void): Err<T, E> { | |
fn(this.#error); | |
return this; | |
} | |
/** | |
* Allows the `Result` to be used in a `for...of` loop. | |
* Does not yield any value for an `Err`. | |
* @example | |
* const failure = err('error'); | |
* for (const value of failure) { | |
* // this loop will not run | |
* } | |
*/ | |
*[Symbol.iterator](): Iterator<T> { | |
// This intentionally does nothing. | |
} | |
/** | |
* Custom `toJSON` implementation for `Err`. | |
* @example | |
* const failure = err('error'); | |
* JSON.stringify(failure); // '{"status":"err","error":"error"}' | |
*/ | |
toJSON(): { status: 'err'; error: E } { | |
return { status: 'err', error: this.#error }; | |
} | |
/** | |
* Returns a string representation of the `Err` object. | |
* @example | |
* const failure = err('error'); | |
* Object.prototype.toString.call(failure); // '[object Err(error)]' | |
*/ | |
get [Symbol.toStringTag](): string { | |
return `Err(${this.#error})`; | |
} | |
} | |
/** | |
* The core `Result` type, which can be either a success (`Ok`) or a failure (`Err`). | |
* @template T The type of the success value. | |
* @template E The type of the error value. | |
*/ | |
export type Result<T, E> = Ok<T, E> | Err<T, E>; | |
/** | |
* Factory function to create a new `Ok` instance. | |
* | |
* @template T The type of the success value. | |
* @template E The type of the error value (defaults to `never`). | |
* @param {T} value The success value to wrap. | |
* @return {Ok<T, E>} A new `Ok` instance containing the value. | |
* @example | |
* const success = ok(42); // Ok(42) | |
* const user = ok({ id: 1, name: 'John' }); // Ok({ id: 1, name: "John" }) | |
*/ | |
export function ok<T, E = never>(value: T): Ok<T, E> { | |
return new Ok(value); | |
} | |
/** | |
* Factory function to create a new `Err` instance. | |
* | |
* @param T The type of the success value (defaults to `never`). | |
* @param E The type of the error value. | |
* @param {E} error The error value to wrap. | |
* @return {Err<T, E>} A new `Err` instance containing the error. | |
* @example | |
* const failure = err('Something went wrong'); // Err("Something went wrong") | |
* const notFound = err({ code: 404, message: 'Not found' }); // Err({ code: 404, message: "Not found" }) | |
*/ | |
export function err<T = never, E = unknown>(error: E): Err<T, E> { | |
return new Err(error); | |
} | |
/** | |
* Converts a `Promise` to a `Promise<Result<T, E>>`. | |
* | |
* @template T The success type of the promise. | |
* @template E The error type to be used in the `Err` case. | |
* @param {PromiseLike<T>} promise The promise to convert. | |
* @param {(e: unknown) => E} errorFn A function to map an unknown thrown error to a specific error type `E`. | |
* @returns {Promise<Result<T, E>>} A promise that will resolve to either an `Ok` or an `Err`. | |
* @example | |
* const apiCall = fetch('/api/users'); | |
* const result = await fromPromise(apiCall, (e) => `API Error: ${e}`); | |
* // result is either Ok(Response) or Err("API Error: ...") | |
* | |
* @example | |
* async function getUser(id: number) { | |
* if (id > 0) { | |
* return { id, name: 'John' }; | |
* } | |
* throw 'Invalid ID'; | |
* } | |
* | |
* const userResult = await fromPromise(getUser(1), (e) => String(e)); // Ok({ id: 1, name: 'John' }) | |
* const errorResult = await fromPromise(getUser(-1), (e) => String(e)); // Err('Invalid ID') | |
*/ | |
export async function fromPromise<T, E>( | |
promise: PromiseLike<T>, | |
errorFn: (e: unknown) => E, | |
): Promise<Result<T, E>> { | |
try { | |
const value = await promise; | |
return new Ok(value); | |
} catch (e) { | |
return new Err(errorFn(e)); | |
} | |
} | |
/** | |
* Wraps a function that may throw an error into a function that returns a `Result`. | |
* | |
* @template T The success type of the function. | |
* @template E The error type to be used in the `Err` case. | |
* @param {() => T} fn The function to wrap. | |
* @param {(e: unknown) => E} errorFn A function to map an unknown thrown error to a specific error type `E`. | |
* @returns {Result<T, E>} A `Result` that is either the return value of `fn` or an error. | |
* @example | |
* const parse = (json: string) => fromThrowable( | |
* () => JSON.parse(json), | |
* (e) => ({ type: 'ParseError', message: String(e) }) | |
* ); | |
* | |
* const result = parse('{"name": "John"}'); // Ok({ name: "John" }) | |
* const result2 = parse('invalid json'); // Err({ type: 'ParseError', ... }) | |
*/ | |
export function fromThrowable<T, E>( | |
fn: () => T, | |
errorFn: (e: unknown) => E, | |
): Result<T, E> { | |
try { | |
return new Ok(fn()); | |
} catch (e) { | |
return new Err(errorFn(e)); | |
} | |
} | |
type ResultOkTypes<T extends ReadonlyArray<Result<unknown, unknown>>> = { | |
[K in keyof T]: T[K] extends Result<infer U, unknown> ? U : never; | |
}; | |
type ResultErrType<T extends ReadonlyArray<Result<unknown, unknown>>> = { | |
[K in keyof T]: T[K] extends Result<unknown, infer E> ? E : never; | |
}[number]; | |
/** | |
* Combines a tuple of `Result`s into a single `Result`. | |
* If all `Result`s are `Ok`, returns an `Ok` with a tuple of the values. | |
* If any `Result` is an `Err`, returns the first `Err`. | |
* | |
* @template T A tuple of `Result` types. | |
* @param {T} results The tuple of `Result`s to combine. | |
* @returns {Result<ResultOkTypes<T>, ResultErrType<T>>} A single `Result`. | |
* @example | |
* const result = combine([ok(1), ok('hello'), ok(true)]); | |
* // result is Ok([1, 'hello', true]) | |
* | |
* const result2 = combine([ok(1), err('error'), ok(true)]); | |
* // result2 is Err('error') | |
*/ | |
export function combine<T extends ReadonlyArray<Result<unknown, unknown>>>( | |
results: [...T], | |
): Result<ResultOkTypes<T>, ResultErrType<T>> { | |
const values: unknown[] = []; | |
for (const r of results) { | |
if (r.isErr()) { | |
return r as unknown as Result<ResultOkTypes<T>, ResultErrType<T>>; | |
} | |
values.push(r.unwrap()); | |
} | |
return ok(values as ResultOkTypes<T>); | |
} | |
/** | |
* Combines an array of `Result`s into a single `Result`. | |
* If all `Result`s are `Ok`, returns an `Ok` with an array of the values. | |
* If any `Result` is an `Err`, returns the first `Err`. | |
* | |
* @template T The success type of the `Result`s. | |
* @template E The error type of the `Result`s. | |
* @param {ReadonlyArray<Result<T, E>>} results The array of `Result`s to combine. | |
* @returns {Result<T[], E>} A single `Result`. | |
* @example | |
* const result = all([ok(1), ok(2), ok(3)]); | |
* // result is Ok([1, 2, 3]) | |
* | |
* const result2 = all([ok(1), err('error'), ok(3)]); | |
* // result2 is Err('error') | |
*/ | |
export function all<T, E>( | |
results: ReadonlyArray<Result<T, E>>, | |
): Result<T[], E> { | |
const values: T[] = []; | |
for (const r of results) { | |
if (r.isErr()) { | |
return r as unknown as Err<T[], E>; | |
} | |
values.push(r.unwrap()); | |
} | |
return ok(values); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment