diff --git a/README.md b/README.md index 4f05bbe..9bbb4f4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The Rust-like `Result` implements the following methods: | orElse | [or_else] | | transpose | [transpose] | -Unlike Rust, JavaScript doesn't have the 'Ownership' feature, so some API like `as_ref` is not necessary. These implementations are not implemented: +Unlike Rust, JavaScript doesn't have the 'Ownership' feature, so some API like `as_ref` are not necessary. These implementations are not implemented: ```md @@ -129,6 +129,44 @@ There is a [proposal] (stage 2) that introduces `Record` and `Tuple` which are c [proposal]: https://github.com/tc39/proposal-record-tuple +## More Helper Functions +### resultify + +Takes a function and returns a version that returns results asynchronously. + +```ts +import fs from 'node:fs/promises'; + +const copyFile1 = resultify(fs.copyFile); +const copyFile2 = resultify()(fs.copyFile); +``` + +### resultify.sync + +Takes a function and returns a version that returns results synchronously. + +```ts +/** + * @throws {Error} Some error messages + */ +function fn(): string { + // do something +} + +const fn1 = resultify.sync(fn); +const fn1 = resultify.sync()(fn); +``` + +In the context where async functions are not allowed, you can use this function to resultify the sync function. + +### resultify.promise + +Takes a promise and returns a new promise that contains a result. + +```ts +const result = await resultify.promise(promise); +``` + ## License MIT diff --git a/src/__tests__/resultify.test.ts b/src/__tests__/resultify.test.ts new file mode 100644 index 0000000..9756030 --- /dev/null +++ b/src/__tests__/resultify.test.ts @@ -0,0 +1,113 @@ +import { resultify } from '../resultify'; + +function syncFn(throws: boolean) { + if (throws) { + throw new Error('Some error message'); + } else { + return 'hello world'; + } +} + +async function asyncFn(throws: boolean) { + if (throws) { + return Promise.reject(new Error('Some error message')); + } else { + return Promise.resolve('hello world'); + } +} + +describe(`Test fn \`${resultify.name}\``, () => { + it('should resultify a sync function', async () => { + const fn = resultify(syncFn); + + const [result1, result2] = await Promise.all([fn(false), fn(true)]); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect((result2.unwrapErr() as Error).message).toBe('Some error message'); + }); + + it('should resultify an async function', async () => { + const fn = resultify(asyncFn); + + const [result1, result2] = await Promise.all([fn(false), fn(true)]); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect((result2.unwrapErr() as Error).message).toBe('Some error message'); + }); + + it('should resultify a sync function with error type specified', async () => { + const fn = resultify()(syncFn); + + const [result1, result2] = await Promise.all([fn(false), fn(true)]); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect(result2.unwrapErr().message).toBe('Some error message'); + }); + + it('should resultify an async function with error type specified', async () => { + const fn = resultify()(asyncFn); + + const [result1, result2] = await Promise.all([fn(false), fn(true)]); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect(result2.unwrapErr().message).toBe('Some error message'); + }); +}); + +describe(`Test fn \`${resultify.sync.name}\``, () => { + it('should resultify a sync function', () => { + const fn = resultify.sync(syncFn); + + const result1 = fn(false); + const result2 = fn(true); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect((result2.unwrapErr() as Error).message).toBe('Some error message'); + }); + + it('should resultify a sync function with error type specified', () => { + const fn = resultify.sync()(syncFn); + + const result1 = fn(false); + const result2 = fn(true); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect(result2.unwrapErr().message).toBe('Some error message'); + }); +}); + +describe(`Test fn \`${resultify.promise.name}\``, () => { + it('should resultify a promise', async () => { + const promise1 = asyncFn(false); + const promise2 = asyncFn(true); + + const [result1, result2] = await Promise.all([ + resultify.promise(promise1), + resultify.promise(promise2), + ]); + + expect(result1.isOk()).toBe(true); + expect(result1.unwrap()).toBe('hello world'); + expect(result2.isErr()).toBe(true); + expect(result2.unwrapErr()).toBeInstanceOf(Error); + expect(result2.unwrapErr().message).toBe('Some error message'); + }); +}); diff --git a/src/index.ts b/src/index.ts index 93f2263..5b73caf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './factory'; +export * from './resultify'; export * from './types'; diff --git a/src/resultify.ts b/src/resultify.ts new file mode 100644 index 0000000..0f7501e --- /dev/null +++ b/src/resultify.ts @@ -0,0 +1,126 @@ +import { Err, Ok } from './factory'; +import type { Result } from './types'; + +type NoVoid = T extends void ? undefined : T; + +/** + * Takes a promise and returns a new promise that contains a result. + * + * Examples: + * ```ts + * const result = await resultify.promise(promise); + * ``` + */ +async function resultifyPromise(promise: Promise): Promise, E>> { + try { + return Ok((await promise) as NoVoid); + } catch (err) { + return Err(err as E); + } +} + +type CurriedResultifySync = ( + fn: (...args: Args) => T, +) => (...args: Args) => Result, E>; + +/** + * Takes a function and returns a version that returns results synchronously. + * + * Examples: + * ```ts + * function fn(): string { + * // throws error if failed + * } + * const fn1 = resultify.sync(fn); + * ``` + * + * In the context where async functions are not allowed, you can use this function to resultify the sync function. + * If you want to resultify an async function, please use `resultify` instead. + * + * If you need the error value and want to specify its type, please use another overloaded function. + */ +function resultifySync(fn: (...args: Args) => T): (...args: Args) => Result, E>; +/** + * Takes a function and returns a version that returns results synchronously. + * + * Examples: + * ```ts + * function fn(): string { + * // throws error if failed + * } + * const fn1 = resultify.sync(fn); + * ``` + * + * In the context where async functions are not allowed, you can use this function to resultify the sync function. + * If you want to resultify an async function, please use `resultify` instead. + */ +function resultifySync(): CurriedResultifySync; + +function resultifySync( + fn?: (...args: Args) => T, +): CurriedResultifySync | ((...args: Args) => Result, E>) { + function curriedResultify(_fn: (...args: TArgs) => TT) { + return function resultifiedFn(...args: TArgs): Result, E> { + try { + return Ok(_fn(...args) as NoVoid); + } catch (err) { + return Err(err as E); + } + }; + } + + return fn ? curriedResultify(fn) : curriedResultify; +} + +type CurriedResultify = ( + fn: (...args: Args) => T | Promise, +) => (...args: Args) => Promise>, E>>; + +/** + * Takes a function and returns a version that returns results asynchronously. + * + * Examples: + * ```ts + * import fs from 'node:fs/promises'; + * + * const copyFile = resultify(fs.copyFile); + * ``` + * + * If you need the error value and want to specify its type, please use another overloaded function. + */ +function resultify( + fn: (...args: Args) => T | Promise, +): (...args: Args) => Promise>, E>>; +/** + * Takes a function and returns a version that returns results asynchronously. + * This overloaded function allows you to specify the error type. + * + * Examples: + * ```ts + * import fs from 'node:fs/promises'; + * + * const copyFile = resultify()(fs.copyFile); + * ``` + */ +function resultify(): CurriedResultify; + +function resultify( + fn?: (...args: Args) => T | Promise, +): CurriedResultify | ((...args: Args) => Promise>, E>>) { + function curriedResultify(_fn: (...args: TArgs) => TT | Promise) { + return async function resultifiedFn(...args: TArgs): Promise>, E>> { + try { + return Ok((await _fn(...args)) as NoVoid>); + } catch (err) { + return Err(err as E); + } + }; + } + + return fn ? curriedResultify(fn) : curriedResultify; +} + +resultify.sync = resultifySync; +resultify.promise = resultifyPromise; + +export { resultify };