Skip to content

Commit

Permalink
Merge branch 'master' into matt/removed-unused-dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
supermacro authored Sep 6, 2024
2 parents 098a203 + fb29403 commit 4cbce01
Show file tree
Hide file tree
Showing 8 changed files with 894 additions and 40 deletions.
5 changes: 0 additions & 5 deletions .changeset/three-mice-act.md

This file was deleted.

13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# neverthrow

## 7.1.0

### Minor Changes

- [#467](https://github.com/supermacro/neverthrow/pull/467) [`4b9d2fd`](https://github.com/supermacro/neverthrow/commit/4b9d2fdaf03223945068509f948b57194732aa03) Thanks [@untidy-hair
](https://github.com/untidy-hair)! - feat: add `andTee` and `andThrough` to handle side-effect

### Patch Changes

- [#483](https://github.com/supermacro/neverthrow/pull/483) [`96f7f66`](https://github.com/supermacro/neverthrow/commit/96f7f669ac83be705a389d47ed804e9d44a13932) Thanks [@braxtonhall](https://github.com/braxtonhall)! - Fix `combineWithAllErrors` types

- [#563](https://github.com/supermacro/neverthrow/pull/563) [`eadf50c`](https://github.com/supermacro/neverthrow/commit/eadf50c695db896b8841c0ee301ae5eeba994b90) Thanks [@mattpocock](https://github.com/mattpocock)! - Made err() infer strings narrowly for easier error tagging.

## 7.0.1

### Patch Changes
Expand Down
229 changes: 229 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`Result.orElse` (method)](#resultorelse-method)
- [`Result.match` (method)](#resultmatch-method)
- [`Result.asyncMap` (method)](#resultasyncmap-method)
- [`Result.andTee` (method)](#resultandtee-method)
- [`Result.andThrough` (method)](#resultandthrough-method)
- [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method)
- [`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)
Expand All @@ -51,6 +54,8 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
- [`ResultAsync.andThen` (method)](#resultasyncandthen-method)
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
- [`ResultAsync.andTee` (method)](#resultasyncandtee-method)
- [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method)
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
- [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap)
Expand Down Expand Up @@ -541,6 +546,136 @@ Note that in the above example if `parseHeaders` returns an `Err` then `.map` an

---

#### `Result.andTee` (method)

Takes a `Result<T, E>` and lets the original `Result<T, E>` pass through regardless the result of the passed-in function.
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.

**Signature:**

```typescript
class Result<T, E> {
andTee(
callback: (value: T) => unknown
): Result<T, E> { ... }
}
```

**Example:**

```typescript
import { parseUserInput } from 'imaginary-parser'
import { logUser } from 'imaginary-logger'
import { insertUser } from 'imaginary-database'

// ^ assume parseUserInput, logUser and insertUser have the following signatures:
// parseUserInput(input: RequestData): Result<User, ParseError>
// logUser(user: User): Result<void, LogError>
// insertUser(user: User): ResultAsync<void, InsertError>
// Note logUser returns void upon success but insertUser takes User type.

const resAsync = parseUserInput(userInput)
.andTee(logUser)
.asyncAndThen(insertUser)

// Note no LogError shows up in the Result type
resAsync.then((res: Result<void, ParseError | InsertError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User input has been parsed and inserted successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---

#### `Result.andThrough` (method)

Similar to `andTee` except for:

- when there is an error from the passed-in function, that error will be passed along.

**Signature:**

```typescript
class Result<T, E> {
andThrough<F>(
callback: (value: T) => Result<unknown, F>
): Result<T, E | F> { ... }
}
```

**Example:**

```typescript
import { parseUserInput } from 'imaginary-parser'
import { validateUser } from 'imaginary-validator'
import { insertUser } from 'imaginary-database'

// ^ assume parseUseInput, validateUser and insertUser have the following signatures:
// parseUserInput(input: RequestData): Result<User, ParseError>
// validateUser(user: User): Result<void, ValidateError>
// insertUser(user: User): ResultAsync<void, InsertError>
// Note validateUser returns void upon success but insertUser takes User type.

const resAsync = parseUserInput(userInput)
.andThrough(validateUser)
.asyncAndThen(insertUser)

resAsync.then((res: Result<void, ParseErro | ValidateError | InsertError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User input has been parsed, validated, inserted successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---

#### `Result.asyncAndThrough` (method)

Similar to `andThrough` except you must return a ResultAsync.

You can then chain the result of `asyncAndThrough` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.)

**Signature:**

```typescript
import { parseUserInput } from 'imaginary-parser'
import { insertUser } from 'imaginary-database'
import { sendNotification } from 'imaginary-service'

// ^ assume parseUserInput, insertUser and sendNotification have the following signatures:
// parseUserInput(input: RequestData): Result<User, ParseError>
// insertUser(user: User): ResultAsync<void, InsertError>
// sendNotification(user: User): ResultAsync<void, NotificationError>
// Note insertUser returns void upon success but sendNotification takes User type.

const resAsync = parseUserInput(userInput)
.asyncAndThrough(insertUser)
.andThen(sendNotification)

resAsync.then((res: Result<void, ParseError | InsertError | NotificationError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User has been parsed, inserted and notified successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---
#### `Result.fromThrowable` (static class method)

> Although Result is not an actual JS class, the way that `fromThrowable` has been implemented requires that you call `fromThrowable` as though it were a static method on `Result`. See examples below.
Expand Down Expand Up @@ -1096,7 +1231,101 @@ const resultMessage = await validateUser(user)
[⬆️ Back to top](#toc)

---
#### `ResultAsync.andTee` (method)

Takes a `ResultAsync<T, E>` and lets the original `ResultAsync<T, E>` pass through regardless
the result of the passed-in function.
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.

**Signature:**

```typescript
class ResultAsync<T, E> {
andTee(
callback: (value: T) => unknown
): ResultAsync<T, E> => { ... }
}
```

**Example:**

```typescript
import { insertUser } from 'imaginary-database'
import { logUser } from 'imaginary-logger'
import { sendNotification } from 'imaginary-service'

// ^ assume insertUser, logUser and sendNotification have the following signatures:
// insertUser(user: User): ResultAsync<User, InsertError>
// logUser(user: User): Result<void, LogError>
// sendNotification(user: User): ResultAsync<void, NotificationError>
// Note logUser returns void on success but sendNotification takes User type.

const resAsync = insertUser(user)
.andTee(logUser)
.andThen(sendNotification)

// Note there is no LogError in the types below
resAsync.then((res: Result<void, InsertError | NotificationError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User has been inserted and notified successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---
#### `ResultAsync.andThrough` (method)


Similar to `andTee` except for:

- when there is an error from the passed-in function, that error will be passed along.

**Signature:**

```typescript
class ResultAsync<T, E> {
andThrough<F>(
callback: (value: T) => Result<unknown, F> | ResultAsync<unknown, F>,
): ResultAsync<T, E | F> => { ... }
}
```

**Example:**

```typescript

import { buildUser } from 'imaginary-builder'
import { insertUser } from 'imaginary-database'
import { sendNotification } from 'imaginary-service'

// ^ assume buildUser, insertUser and sendNotification have the following signatures:
// buildUser(userRaw: UserRaw): ResultAsync<User, BuildError>
// insertUser(user: User): ResultAsync<void, InsertError>
// sendNotification(user: User): ResultAsync<void, NotificationError>
// Note insertUser returns void upon success but sendNotification takes User type.

const resAsync = buildUser(userRaw)
.andThrough(insertUser)
.andThen(sendNotification)

resAsync.then((res: Result<void, BuildError | InsertError | NotificationError>) => {e
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User data has been built, inserted and notified successfully.")
}
}))
```

[⬆️ Back to top](#toc)

---
#### `ResultAsync.combine` (static class method)

Combine lists of `ResultAsync`s.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "neverthrow",
"version": "7.0.1",
"version": "7.1.0",
"description": "Stop throwing errors, and instead return Results!",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
Expand Down
53 changes: 38 additions & 15 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
)
}

andThrough<F>(f: (t: T) => Result<unknown, F> | ResultAsync<unknown, F>): ResultAsync<T, E | F> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
if (res.isErr()) {
return new Err<T, E>(res.error)
}

const newRes = await f(res.value)
if (newRes.isErr()) {
return new Err<T, F>(newRes.error)
}
return new Ok<T, F>(res.value)
}),
)
}

andTee(f: (t: T) => unknown): ResultAsync<T, E> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
if (res.isErr()) {
return new Err<T, E>(res.error)
}
try {
await f(res.value)
} catch (e) {
// Tee does not care about the error
}
return new Ok<T, E>(res.value)
}),
)
}

mapErr<U>(f: (e: E) => U | Promise<U>): ResultAsync<T, U> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
Expand Down Expand Up @@ -246,22 +278,13 @@ type TraverseAsync<T, Depth extends number = 5> = IsLiteralArray<T> extends 1
: never

// This type is similar to the `TraverseAsync` while the errors are also
// collected in order. For the checks/conditions made here, see that type
// collected in a list. For the checks/conditions made here, see that type
// for the documentation.
type TraverseWithAllErrorsAsync<T, Depth extends number = 5> = IsLiteralArray<T> extends 1
? Combine<T, Depth> extends [infer Oks, infer Errs]
? ResultAsync<EmptyArrayToNever<Oks>, EmptyArrayToNever<Errs>>
: never
: Writable<T> extends Array<infer I>
? Combine<MemberListOf<I>, Depth> extends [infer Oks, infer Errs]
? Oks extends unknown[]
? Errs extends unknown[]
? ResultAsync<EmptyArrayToNever<Oks[number][]>, EmptyArrayToNever<Errs[number][]>>
: ResultAsync<EmptyArrayToNever<Oks[number][]>, Errs>
: Errs extends unknown[]
? ResultAsync<Oks, EmptyArrayToNever<Errs[number][]>>
: ResultAsync<Oks, Errs>
: never
type TraverseWithAllErrorsAsync<T, Depth extends number = 5> = TraverseAsync<
T,
Depth
> extends ResultAsync<infer Oks, infer Errs>
? ResultAsync<Oks, Errs[]>
: never

// Converts a reaodnly array into a writable array
Expand Down
Loading

0 comments on commit 4cbce01

Please sign in to comment.