Skip to content

feat: Add a function that combines multiple results and returns an object #610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thin-wolves-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'neverthrow': minor
---

Add a function that combines multiple results and returns an object.
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method)
- [`Result.combine` (static class method)](#resultcombine-static-class-method)
- [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method)
- [`Result.struct` (static class method)](#resultstruct-static-class-method)
- [`Result.safeUnwrap()`](#resultsafeunwrap)
+ [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync)
- [`okAsync`](#okasync)
Expand All @@ -58,6 +59,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method)
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
- [`ResultAsync.struct` (static class method)](#resultasyncstruct-static-class-method)
- [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap)
+ [Utilities](#utilities)
- [`fromThrowable`](#fromthrowable)
Expand Down Expand Up @@ -800,6 +802,59 @@ const result = Result.combineWithAllErrors(resultList)

[⬆️ Back to top](#toc)

---

#### `Result.struct` (static class method)

> Although Result is not an actual JS class, the way that `struct` has been implemented requires that you call `struct` as though it were a static method on `Result`. See examples below.

Combine objects of `Result`s.

**`struct` works on both heterogeneous and homogeneous objects**. This means that you can have objects that contain different kinds of `Result`s and still be able to combine them. Note that you cannot combine objects that contain both `Result`s **and** `ResultAsync`s.

The `struct` function takes an object of results and returns a single result. If all the results in the object are `Ok`, then the return value will be a `Ok` containing an object of all the individual `Ok` values.

If multiple results in the object are `Err` then the `struct` function returns an `Err` containing an array of all the error values.

Example:
```typescript
const resultObject: {
a: Result<number, never>
b: Result<number, never>
} = {
a: ok(1),
b: ok(2),
}

const combinedList: Result<{
a: number
b: number
}, never> = Result.struct(resultObject)
```

Example of error:
```typescript
const resultObject: {
a: Result<number, unknown>
b: Result<never, number>
c: Result<never, string>
} = {
a: ok(1),
b: err(2),
c: err('3'),
}

const combinedList: Result<{
a: number
b: unknown
c: unknown
}, (number | number)[]> = Result.struct(resultObject)
```

[⬆️ Back to top](#toc)

---

#### `Result.safeUnwrap()`

**Deprecated**. You don't need to use this method anymore.
Expand Down Expand Up @@ -1410,6 +1465,57 @@ const result = ResultAsync.combineWithAllErrors(resultList)
// result is Err(['boooom!', 'ahhhhh!'])
```

---

#### `ResultAsync.struct` (static class method)

Combine objects of `ResultAsyncs`s.

**`struct` works on both heterogeneous and homogeneous objects**. This means that you can have objects that contain different kinds of `Result`s and still be able to combine them. Note that unlike `Result.struct`, you can combine objects containing both `Result`s and `ResultAsync`s.

The `struct` function takes an object of results and returns a single result. If all the results in the object are `Ok`, then the return value will be a `Ok` containing an object of all the individual `Ok` values.

If multiple results in the object are `Err` then the `struct` function returns an `Err` containing an array of all the error values.

Example:
```typescript
const resultObject: {
a: ResultAsync<number, never>
b: ResultAsync<number, never>
} = {
a: okAsync(1),
b: okAsync(2),
}

const combinedList: ResultAsync<{
a: number
b: number
}, never> = ResultAsync.struct(resultObject)
```

Example of error:
```typescript
const resultObject: {
a: ResultAsync<number, unknown>
b: ResultAsync<never, number>
c: ResultAsync<never, string>
} = {
a: okAsync(1),
b: errAsync(2),
c: errAsync('3'),
}

const combinedList: ResultAsync<{
a: number
b: unknown
c: unknown
}, (number | number)[]> = ResultAsync.struct(resultObject)
```

[⬆️ Back to top](#toc)

---

#### `ResultAsync.safeUnwrap()`

**Deprecated**. You don't need to use this method anymore.
Expand Down
5 changes: 5 additions & 0 deletions src/_internals/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ export type ExtractErrAsyncTypes<T extends readonly ResultAsync<unknown, unknown
[idx in keyof T]: T[idx] extends ResultAsync<unknown, infer E> ? E : never
}

export type MaybeResultAsync<T, E> = ResultAsync<T, E> | Result<T, E>

export type InferOkTypes<R> = R extends Result<infer T, unknown> ? T : never
export type InferErrTypes<R> = R extends Result<unknown, infer E> ? E : never

export type InferAsyncOkTypes<R> = R extends ResultAsync<infer T, unknown> ? T : never
export type InferAsyncErrTypes<R> = R extends ResultAsync<unknown, infer E> ? E : never

export type InferMaybeAsyncOkTypes<R> = R extends MaybeResultAsync<infer T, unknown> ? T : never
export type InferMaybeAsyncErrTypes<R> = R extends MaybeResultAsync<unknown, infer E> ? E : never

/**
* Short circuits on the FIRST Err value that we find
*/
Expand Down
40 changes: 40 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
InferAsyncErrTypes,
InferAsyncOkTypes,
InferErrTypes,
InferMaybeAsyncErrTypes,
InferMaybeAsyncOkTypes,
InferOkTypes,
MaybeResultAsync,
} from './_internals/utils'

export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
Expand Down Expand Up @@ -86,6 +89,35 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
) as CombineResultsWithAllErrorsArrayAsync<T>
}

static struct<E, T extends Record<string, MaybeResultAsync<unknown, E>>>(
record: T,
): StructResultAsync<E, T> {
const results = Object.entries(record).reduce<
Array<MaybeResultAsync<[string, unknown], unknown>>
>((previous, [key, value]) => {
previous.push(value.map((result) => [key, result]))
return previous
}, [])

return ResultAsync.fromSafePromise(Promise.all(results)).andThen((results) => {
const errors = results.filter((result) => result.isErr())
if (0 < errors.length) {
return errAsync(
errors.map((error) => (error as Err<unknown, InferMaybeAsyncErrTypes<T[keyof T]>>).error),
)
}

const successes = results as Ok<[string, unknown], unknown>[]
return okAsync(
successes.reduce<Record<string, unknown>>((previous, result) => {
const [key, value] = result.value
previous[key] = value
return previous
}, {}) as { [Key in keyof T]: InferMaybeAsyncOkTypes<T[Key]> },
)
})
}

map<A>(f: (t: T) => A | Promise<A>): ResultAsync<A, E> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
Expand Down Expand Up @@ -259,6 +291,14 @@ export type CombineResultsWithAllErrorsArrayAsync<
? TraverseWithAllErrorsAsync<UnwrapAsync<T>>
: ResultAsync<ExtractOkAsyncTypes<T>, ExtractErrAsyncTypes<T>[number][]>

export type StructResultAsync<
E,
T extends Record<string, MaybeResultAsync<unknown, E>>
> = ResultAsync<
{ [Key in keyof T]: InferMaybeAsyncOkTypes<T[Key]> },
Array<InferMaybeAsyncErrTypes<T[keyof T]>>
>

// Unwraps the inner `Result` from a `ResultAsync` for all elements.
type UnwrapAsync<T> = IsLiteralArray<T> extends 1
? Writable<T> extends [infer H, ...infer Rest]
Expand Down
22 changes: 22 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ export namespace Result {
): CombineResultsWithAllErrorsArray<T> {
return combineResultListWithAllErrors(resultList) as CombineResultsWithAllErrorsArray<T>
}

export function struct<E, T extends Record<string, Result<unknown, E>>>(
record: T,
): StructResult<E, T> {
const errors = Object.values(record).filter((result) => result.isErr())
if (0 < errors.length) {
return err(errors.map((error) => (error as Err<unknown, InferErrTypes<T[keyof T]>>).error))
}

const successes = Object.entries(record) as [string, Ok<T[keyof T], unknown>][]
return ok(
successes.reduce<Record<string, unknown>>((previous, [key, result]) => {
previous[key] = result.value
return previous
}, {}) as { [Key in keyof T]: InferOkTypes<T[Key]> },
)
}
}

export type Result<T, E> = Ok<T, E> | Err<T, E>
Expand Down Expand Up @@ -697,4 +714,9 @@ export type CombineResultsWithAllErrorsArray<
? TraverseWithAllErrors<T>
: Result<ExtractOkTypes<T>, ExtractErrTypes<T>[number][]>

export type StructResult<E, T extends Record<string, Result<unknown, E>>> = Result<
{ [Key in keyof T]: InferOkTypes<T[Key]> },
Array<InferErrTypes<T[keyof T]>>
>

//#endregion
98 changes: 98 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,104 @@ describe('Utils', () => {
})
})
})
describe('`Result.struct`', () => {
it('returns Ok with all values when all results are Ok', () => {
const input = {
a: ok(1),
b: ok('test'),
c: ok(true),
}

const result = Result.struct(input)

expect(result.isOk()).toBe(true)
expect(result._unsafeUnwrap()).toEqual({ a: 1, b: 'test', c: true })
})

it('returns Err with a single error when one result is Err', () => {
const input = {
a: ok(1),
b: err('error1'),
c: ok(true),
}

const result = Result.struct(input)

expect(result.isErr()).toBe(true)
expect(result._unsafeUnwrapErr()).toEqual(['error1'])
})

it('returns Err with all errors when some results are Err', () => {
const input = {
a: ok(1),
b: err('error1'),
c: err('error2'),
}

const result = Result.struct(input)

expect(result.isErr()).toBe(true)
expect(result._unsafeUnwrapErr()).toEqual(['error1', 'error2'])
})

it('returns Ok with an empty object when input is an empty object', () => {
const input = {}

const result = Result.struct(input)

expect(result.isOk()).toBe(true)
expect(result.unwrapOr({})).toEqual({})
})
})
describe('`ResultAsync.struct`', () => {
it('returns Ok with all values when all results are Ok', async () => {
const input = {
a: okAsync(1),
b: okAsync('test'),
c: okAsync(true),
}

const result = await ResultAsync.struct(input)

expect(result.isOk()).toBe(true)
expect(result._unsafeUnwrap()).toEqual({ a: 1, b: 'test', c: true })
})

it('returns Err with a single error when one result is Err', async () => {
const input = {
a: okAsync(1),
b: errAsync('error1'),
c: okAsync(true),
}

const result = await ResultAsync.struct(input)

expect(result.isErr()).toBe(true)
expect(result._unsafeUnwrapErr()).toEqual(['error1'])
})

it('returns Err with all errors when some results are Err', async () => {
const input = {
a: okAsync(1),
b: errAsync('error1'),
c: errAsync('error2'),
}

const result = await ResultAsync.struct(input)

expect(result.isErr()).toBe(true)
expect(result._unsafeUnwrapErr()).toEqual(['error1', 'error2'])
})

it('returns Ok with an empty object when input is an empty object', async () => {
const input = {}

const result = await ResultAsync.struct(input)

expect(result.isOk()).toBe(true)
expect(result.unwrapOr({})).toEqual({})
})
})
})

describe('ResultAsync', () => {
Expand Down