From 41fc3b0a06cee3686771101493dc00186249fabe Mon Sep 17 00:00:00 2001 From: b-michalkiewicz Date: Fri, 16 Feb 2024 11:38:28 +0100 Subject: [PATCH 1/2] feat: add partition static function --- README.md | 86 ++++++++++++++++ src/_internals/utils.ts | 11 ++ src/result-async.ts | 17 ++++ src/result.ts | 18 ++++ tests/index.test.ts | 218 ++++++++++++++++++++++++++++++---------- 5 files changed, 296 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index aca2bd11..8dbd4bb5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`Result.combine` (static class method)](#resultcombine-static-class-method) - [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method) - [`Result.safeUnwrap()`](#resultsafeunwrap) + - [`Result.partition (static class method)`](#resultpartition-static-class-method) + [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync) - [`okAsync`](#okasync) - [`errAsync`](#errasync) @@ -53,6 +54,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method) - [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method) - [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap) + - [`ResultAsync.partition (static class method)`](#resultasyncpartition-static-class-method) + [Utilities](#utilities) - [`fromThrowable`](#fromthrowable) - [`fromPromise`](#frompromise) @@ -669,6 +671,49 @@ const result = Result.combineWithAllErrors(resultList) Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate. +[⬆️ Back to top](#toc) + +--- + +#### `Result.partition` (static class method) + +> Although Result is not an actual JS class, the way that `partition` has been implemented requires that you call `partition` as though it were a static method on `Result`. See examples below. + +Partition lists of `Result`s. + +**`partition` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `Result`s and still be able to partition them. Note that you cannot partition lists that contain both `Result`s **and** `ResultAsync`s. + +The `partition` function takes a list of results and returns a tuple of all `Ok` results and `Err` errors. + +Function signature: + +```typescript +// homogeneous lists +function partition(resultList: Result[]): [T[], E[]] + +// heterogeneous lists +function partition(resultList: [ Result, Result ]): [(T1 | T2)[], (E1 | E2)[]] +function partition => [(T1 | T2 | T3) [], (E1 | E2 | E3)[]] +function partition => [(T1 | T2 | T3 | T4)[], (E1 | E2 | E3 | E4)[]] +// ... etc etc ad infinitum +``` + +Example usage: + +```typescript +const resultList: Result[] = [ + ok(123), + err('boooom!'), + ok(456), + err('ahhhhh!'), +] + +const [results, errors] = Result.partition(resultList) + +// results is [123, 456] +// errors is ['boooom!', 'ahhhhh!'] +``` + [⬆️ Back to top](#toc) --- @@ -1148,6 +1193,47 @@ Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reduc --- +#### `ResultAsync.partition` (static class method) + +Partition lists of `ResultAsync`s. + +**`partition` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `ResultAsync`s and still be able to partition them. Note that you cannot partition lists that contain both `Result`s **and** `ResultAsync`s. + +The `partition` function takes a list of async results and returns a `Promise` of a tuple of all Ok results and Err errors. + +Function signature: + +```typescript +// homogeneous lists +function partition(resultList: ResultAsync[]): Promise<[T[], E[]]> + +// heterogeneous lists +function partition(resultList: [ ResultAsync, ResultAsync ]): Promise<[(T1 | T2)[], (E1 | E2)[]]> +function partition => Promise<[(T1 | T2 | T3)[], (E1 | E2 | E3)[]]> +function partition => Promise<[(T1 | T2 | T3 | T4)[], (E1 | E2 | E3 | E4)[]]> +// ... etc etc ad infinitum +``` + +Example usage: + +```typescript +const resultList: ResultAsync[] = [ + okAsync(123), + errAsync('boooom!'), + okAsync(456), + errAsync('ahhhhh!'), +] + +const [results, errors] = ResultAsync.partition(resultList) + +// results is [123, 456] +// errors is ['boooom!', 'ahhhhh!'] +``` + +[⬆️ Back to top](#toc) + +--- + ### Utilities #### `fromThrowable` diff --git a/src/_internals/utils.ts b/src/_internals/utils.ts index 6c108b0d..2ba892fa 100644 --- a/src/_internals/utils.ts +++ b/src/_internals/utils.ts @@ -81,3 +81,14 @@ export const combineResultAsyncListWithAllErrors = ( ResultAsync.fromSafePromise(Promise.all(asyncResultList)).andThen( combineResultListWithAllErrors, ) as ResultAsync + +export const partitionResultList = (resultList: readonly Result[]): [T[], E[]] => + resultList.reduce( + ([oks, errors], result) => + result.isErr() ? [oks, [...errors, result.error]] : [[...oks, result.value], errors], + [[], []] as [T[], E[]], + ) + +export const partitionResultAsyncList = ( + asyncResultList: readonly ResultAsync[], +): Promise<[T[], E[]]> => Promise.all(asyncResultList).then(partitionResultList) diff --git a/src/result-async.ts b/src/result-async.ts index 78130820..f8a14064 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -17,6 +17,7 @@ import { InferAsyncOkTypes, InferErrTypes, InferOkTypes, + partitionResultAsyncList, } from './_internals/utils' export class ResultAsync implements PromiseLike> { @@ -68,6 +69,18 @@ export class ResultAsync implements PromiseLike> { ) as CombineResultsWithAllErrorsArrayAsync } + static partition< + T extends readonly [ResultAsync, ...ResultAsync[]] + >(asyncResultList: T): PartitionResultAsync + static partition[]>( + asyncResultList: T, + ): PartitionResultAsync + static partition[]>( + asyncResultList: T, + ): PartitionResultAsync { + return partitionResultAsyncList(asyncResultList) as PartitionResultAsync + } + map(f: (t: T) => A | Promise): ResultAsync { return new ResultAsync( this._promise.then(async (res: Result) => { @@ -176,6 +189,10 @@ export type CombineResultsWithAllErrorsArrayAsync< ? TraverseWithAllErrorsAsync> : ResultAsync, ExtractErrAsyncTypes[number][]> +export type PartitionResultAsync[]> = Promise< + [ExtractOkAsyncTypes[number][], ExtractErrAsyncTypes[number][]] +> + // Unwraps the inner `Result` from a `ResultAsync` for all elements. type UnwrapAsync = IsLiteralArray extends 1 ? Writable extends [infer H, ...infer Rest] diff --git a/src/result.ts b/src/result.ts index f0dac3dc..c17dd47d 100644 --- a/src/result.ts +++ b/src/result.ts @@ -7,6 +7,7 @@ import { ExtractOkTypes, InferErrTypes, InferOkTypes, + partitionResultList, } from './_internals/utils' // eslint-disable-next-line @typescript-eslint/no-namespace @@ -56,6 +57,18 @@ export namespace Result { ): CombineResultsWithAllErrorsArray { return combineResultListWithAllErrors(resultList) as CombineResultsWithAllErrorsArray } + + export function partition< + T extends readonly [Result, ...Result[]] + >(resultList: T): PartitionResult + export function partition[]>( + resultList: T, + ): PartitionResult + export function partition[]>( + resultList: T, + ): PartitionResult { + return partitionResultList(resultList) as PartitionResult + } } export type Result = Ok | Err @@ -583,4 +596,9 @@ export type CombineResultsWithAllErrorsArray< ? TraverseWithAllErrors : Result, ExtractErrTypes[number][]> +export type PartitionResult[]> = [ + ExtractOkTypes[number][], + ExtractErrTypes[number][], +] + //#endregion diff --git a/tests/index.test.ts b/tests/index.test.ts index 7d2d056e..5e64425e 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -332,15 +332,15 @@ describe('Result.fromThrowable', () => { // Added for issue #300 -- the test here is not so much that expectations are met as that the test compiles. it('Accepts an inner function which takes arguments', () => { - const hello = (fname: string): string => `hello, ${fname}`; - const safeHello = Result.fromThrowable(hello); + const hello = (fname: string): string => `hello, ${fname}` + const safeHello = Result.fromThrowable(hello) - const result = hello('Dikembe'); - const safeResult = safeHello('Dikembe'); + const result = hello('Dikembe') + const safeResult = safeHello('Dikembe') - expect(safeResult).toBeInstanceOf(Ok); - expect(result).toEqual(safeResult._unsafeUnwrap()); - }); + expect(safeResult).toBeInstanceOf(Ok) + expect(result).toEqual(safeResult._unsafeUnwrap()) + }) it('Creates a function that returns an err when the inner function throws', () => { const thrower = (): string => { @@ -375,7 +375,7 @@ describe('Result.fromThrowable', () => { }) it('has a top level export', () => { - expect(fromThrowable).toBe(Result.fromThrowable) + expect(fromThrowable).toBe(Result.fromThrowable) }) }) @@ -406,15 +406,15 @@ describe('Utils', () => { }) it('Combines heterogeneous lists', () => { - type HeterogenousList = [ Result, Result, Result ] - - const heterogenousList: HeterogenousList = [ - ok('Yooooo'), - ok(123), - ok(true), + type HeterogenousList = [ + Result, + Result, + Result, ] - type ExpecteResult = Result<[ string, number, boolean ], string | number | boolean> + const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)] + + type ExpecteResult = Result<[string, number, boolean], string | number | boolean> const result: ExpecteResult = Result.combine(heterogenousList) @@ -422,21 +422,18 @@ describe('Utils', () => { }) it('Does not destructure / concatenate arrays', () => { - type HomogenousList = [ - Result, - Result, - ] + type HomogenousList = [Result, Result] - const homogenousList: HomogenousList = [ - ok(['hello', 'world']), - ok([1, 2, 3]) - ] + const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])] - type ExpectedResult = Result<[ string[], number[] ], boolean | string> + type ExpectedResult = Result<[string[], number[]], boolean | string> const result: ExpectedResult = Result.combine(homogenousList) - expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]]) + expect(result._unsafeUnwrap()).toEqual([ + ['hello', 'world'], + [1, 2, 3], + ]) }) }) @@ -445,7 +442,7 @@ describe('Utils', () => { const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)] const resultAsync: ResultAsync = ResultAsync.combine(asyncResultList) - + expect(resultAsync).toBeInstanceOf(ResultAsync) const result = await ResultAsync.combine(asyncResultList) @@ -480,14 +477,14 @@ describe('Utils', () => { okAsync('Yooooo'), okAsync(123), okAsync(true), - okAsync([ 1, 2, 3]), + okAsync([1, 2, 3]), ] - type ExpecteResult = Result<[ string, number, boolean, number[] ], string | number | boolean> + type ExpecteResult = Result<[string, number, boolean, number[]], string | number | boolean> const result: ExpecteResult = await ResultAsync.combine(heterogenousList) - expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [ 1, 2, 3 ]]) + expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [1, 2, 3]]) }) }) }) @@ -517,15 +514,15 @@ describe('Utils', () => { }) it('Combines heterogeneous lists', () => { - type HeterogenousList = [ Result, Result, Result ] - - const heterogenousList: HeterogenousList = [ - ok('Yooooo'), - ok(123), - ok(true), + type HeterogenousList = [ + Result, + Result, + Result, ] - type ExpecteResult = Result<[ string, number, boolean ], (string | number | boolean)[]> + const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)] + + type ExpecteResult = Result<[string, number, boolean], (string | number | boolean)[]> const result: ExpecteResult = Result.combineWithAllErrors(heterogenousList) @@ -533,21 +530,18 @@ describe('Utils', () => { }) it('Does not destructure / concatenate arrays', () => { - type HomogenousList = [ - Result, - Result, - ] + type HomogenousList = [Result, Result] - const homogenousList: HomogenousList = [ - ok(['hello', 'world']), - ok([1, 2, 3]) - ] + const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])] - type ExpectedResult = Result<[ string[], number[] ], (boolean | string)[]> + type ExpectedResult = Result<[string[], number[]], (boolean | string)[]> const result: ExpectedResult = Result.combineWithAllErrors(homogenousList) - expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]]) + expect(result._unsafeUnwrap()).toEqual([ + ['hello', 'world'], + [1, 2, 3], + ]) }) }) describe('`ResultAsync.combineWithAllErrors`', () => { @@ -575,15 +569,15 @@ describe('Utils', () => { }) it('Combines heterogeneous lists', async () => { - type HeterogenousList = [ ResultAsync, ResultAsync, ResultAsync ] - - const heterogenousList: HeterogenousList = [ - okAsync('Yooooo'), - okAsync(123), - okAsync(true), + type HeterogenousList = [ + ResultAsync, + ResultAsync, + ResultAsync, ] - type ExpecteResult = Result<[ string, number, boolean ], [string, number, boolean]> + const heterogenousList: HeterogenousList = [okAsync('Yooooo'), okAsync(123), okAsync(true)] + + type ExpecteResult = Result<[string, number, boolean], [string, number, boolean]> const result: ExpecteResult = await ResultAsync.combineWithAllErrors(heterogenousList) @@ -612,6 +606,123 @@ describe('Utils', () => { }) }) }) + describe('`Result.partition`', () => { + describe('Synchronous `partition`', () => { + it('Partitions a list of results into an Ok value', () => { + const resultList = [ok(123), ok(456), ok(789)] + + const [results, errors] = Result.partition(resultList) + + expect(results).toEqual([123, 456, 789]) + expect(errors).toEqual([]) + }) + + it('Partitions a list of results into an Err value', () => { + const resultList: Result[] = [ + ok(123), + err('boooom!'), + ok(456), + err('ahhhhh!'), + ] + + const [results, errors] = Result.partition(resultList) + + expect(results).toEqual([123, 456]) + expect(errors).toEqual(['boooom!', 'ahhhhh!']) + }) + + it('Partitions heterogeneous lists', () => { + type HeterogenousList = [ + Result, + Result, + Result, + ] + + const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)] + + type ExpecteResult = [(string | number | boolean)[], (string | number | boolean)[]] + + const [results, errors]: ExpecteResult = Result.partition(heterogenousList) + + expect(results).toEqual(['Yooooo', 123, true]) + expect(errors).toEqual([]) + }) + + it('Does not destructure / concatenate arrays', () => { + type HomogenousList = [Result, Result] + + const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])] + + type ExpectedResult = [(string[] | number[])[], (boolean | string)[]] + + const [results, errors]: ExpectedResult = Result.partition(homogenousList) + + expect(results).toEqual([ + ['hello', 'world'], + [1, 2, 3], + ]) + expect(errors).toEqual([]) + }) + }) + describe('`ResultAsync.partition`', () => { + it('Partitions a list of async results into an Ok value', async () => { + const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)] + + const [results, errors] = await ResultAsync.partition(asyncResultList) + + expect(results).toEqual([123, 456, 789]) + expect(errors).toEqual([]) + }) + + it('Partitions a list of results into an Err value', async () => { + const asyncResultList: ResultAsync[] = [ + okAsync(123), + errAsync('boooom!'), + okAsync(456), + errAsync('ahhhhh!'), + ] + + const [results, errors] = await ResultAsync.partition(asyncResultList) + + expect(results).toEqual([123, 456]) + expect(errors).toEqual(['boooom!', 'ahhhhh!']) + }) + + it('Partitions heterogeneous lists', async () => { + type HeterogenousList = [ + ResultAsync, + ResultAsync, + ResultAsync, + ] + + const heterogenousList: HeterogenousList = [okAsync('Yooooo'), okAsync(123), okAsync(true)] + + type ExpecteResult = [(string | number | boolean)[], (string | number | boolean)[]] + + const [results, errors]: ExpecteResult = await ResultAsync.partition(heterogenousList) + + expect(results).toEqual(['Yooooo', 123, true]) + expect(errors).toEqual([]) + }) + }) + + describe('testdouble `ResultAsync.partition`', () => { + interface ITestInterface { + getName(): string + setName(name: string): void + getAsyncResult(): ResultAsync + } + + it('Partitions `testdouble` proxies from mocks generated via interfaces', async () => { + const mock = td.object() + + const [results] = await ResultAsync.partition([okAsync(mock)] as const) + + expect(results.length).toBe(1) + expect(results[0]).toBe(mock) + }) + }) + }) }) describe('ResultAsync', () => { @@ -831,7 +942,6 @@ describe('ResultAsync', () => { const okVal = okAsync(12) const errorCallback = jest.fn((_errVal) => errAsync('It is now a string')) - const result = await okVal.orElse(errorCallback) expect(result).toEqual(ok(12)) From efe87d82af09c0bb5561eff97e5f5428733b87e0 Mon Sep 17 00:00:00 2001 From: b-michalkiewicz Date: Thu, 22 Feb 2024 07:25:12 +0100 Subject: [PATCH 2/2] fix ResultAsync.partition example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dbd4bb5..d8aa5745 100644 --- a/README.md +++ b/README.md @@ -1224,7 +1224,7 @@ const resultList: ResultAsync[] = [ errAsync('ahhhhh!'), ] -const [results, errors] = ResultAsync.partition(resultList) +const [results, errors] = await ResultAsync.partition(resultList) // results is [123, 456] // errors is ['boooom!', 'ahhhhh!']