Skip to content

Instantly share code, notes, and snippets.

@kukalajet
Created June 26, 2025 11:20
Show Gist options
  • Save kukalajet/6d517bb03acfad575a1a1373479e5d25 to your computer and use it in GitHub Desktop.
Save kukalajet/6d517bb03acfad575a1a1373479e5d25 to your computer and use it in GitHub Desktop.
Result
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([]));
});
});
/* 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