Skip to content

Commit e72e9cd

Browse files
authored
Merge pull request #467 from untidy-hair/feature/andTee
andTee() / andThrough() to handle side-effects a bit easier (from issue #445)
2 parents 2e25831 + 4b9d2fd commit e72e9cd

File tree

5 files changed

+835
-0
lines changed

5 files changed

+835
-0
lines changed

README.md

+229
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
3535
- [`Result.orElse` (method)](#resultorelse-method)
3636
- [`Result.match` (method)](#resultmatch-method)
3737
- [`Result.asyncMap` (method)](#resultasyncmap-method)
38+
- [`Result.andTee` (method)](#resultandtee-method)
39+
- [`Result.andThrough` (method)](#resultandthrough-method)
40+
- [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method)
3841
- [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method)
3942
- [`Result.combine` (static class method)](#resultcombine-static-class-method)
4043
- [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method)
@@ -51,6 +54,8 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
5154
- [`ResultAsync.andThen` (method)](#resultasyncandthen-method)
5255
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
5356
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
57+
- [`ResultAsync.andTee` (method)](#resultasyncandtee-method)
58+
- [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method)
5459
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
5560
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
5661
- [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap)
@@ -541,6 +546,136 @@ Note that in the above example if `parseHeaders` returns an `Err` then `.map` an
541546

542547
---
543548

549+
#### `Result.andTee` (method)
550+
551+
Takes a `Result<T, E>` and lets the original `Result<T, E>` pass through regardless the result of the passed-in function.
552+
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.
553+
554+
**Signature:**
555+
556+
```typescript
557+
class Result<T, E> {
558+
andTee(
559+
callback: (value: T) => unknown
560+
): Result<T, E> { ... }
561+
}
562+
```
563+
564+
**Example:**
565+
566+
```typescript
567+
import { parseUserInput } from 'imaginary-parser'
568+
import { logUser } from 'imaginary-logger'
569+
import { insertUser } from 'imaginary-database'
570+
571+
// ^ assume parseUserInput, logUser and insertUser have the following signatures:
572+
// parseUserInput(input: RequestData): Result<User, ParseError>
573+
// logUser(user: User): Result<void, LogError>
574+
// insertUser(user: User): ResultAsync<void, InsertError>
575+
// Note logUser returns void upon success but insertUser takes User type.
576+
577+
const resAsync = parseUserInput(userInput)
578+
.andTee(logUser)
579+
.asyncAndThen(insertUser)
580+
581+
// Note no LogError shows up in the Result type
582+
resAsync.then((res: Result<void, ParseError | InsertError>) => {e
583+
if(res.isErr()){
584+
console.log("Oops, at least one step failed", res.error)
585+
}
586+
else{
587+
console.log("User input has been parsed and inserted successfully.")
588+
}
589+
}))
590+
```
591+
592+
[⬆️ Back to top](#toc)
593+
594+
---
595+
596+
#### `Result.andThrough` (method)
597+
598+
Similar to `andTee` except for:
599+
600+
- when there is an error from the passed-in function, that error will be passed along.
601+
602+
**Signature:**
603+
604+
```typescript
605+
class Result<T, E> {
606+
andThrough<F>(
607+
callback: (value: T) => Result<unknown, F>
608+
): Result<T, E | F> { ... }
609+
}
610+
```
611+
612+
**Example:**
613+
614+
```typescript
615+
import { parseUserInput } from 'imaginary-parser'
616+
import { validateUser } from 'imaginary-validator'
617+
import { insertUser } from 'imaginary-database'
618+
619+
// ^ assume parseUseInput, validateUser and insertUser have the following signatures:
620+
// parseUserInput(input: RequestData): Result<User, ParseError>
621+
// validateUser(user: User): Result<void, ValidateError>
622+
// insertUser(user: User): ResultAsync<void, InsertError>
623+
// Note validateUser returns void upon success but insertUser takes User type.
624+
625+
const resAsync = parseUserInput(userInput)
626+
.andThrough(validateUser)
627+
.asyncAndThen(insertUser)
628+
629+
resAsync.then((res: Result<void, ParseErro | ValidateError | InsertError>) => {e
630+
if(res.isErr()){
631+
console.log("Oops, at least one step failed", res.error)
632+
}
633+
else{
634+
console.log("User input has been parsed, validated, inserted successfully.")
635+
}
636+
}))
637+
```
638+
639+
[⬆️ Back to top](#toc)
640+
641+
---
642+
643+
#### `Result.asyncAndThrough` (method)
644+
645+
Similar to `andThrough` except you must return a ResultAsync.
646+
647+
You can then chain the result of `asyncAndThrough` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.)
648+
649+
**Signature:**
650+
651+
```typescript
652+
import { parseUserInput } from 'imaginary-parser'
653+
import { insertUser } from 'imaginary-database'
654+
import { sendNotification } from 'imaginary-service'
655+
656+
// ^ assume parseUserInput, insertUser and sendNotification have the following signatures:
657+
// parseUserInput(input: RequestData): Result<User, ParseError>
658+
// insertUser(user: User): ResultAsync<void, InsertError>
659+
// sendNotification(user: User): ResultAsync<void, NotificationError>
660+
// Note insertUser returns void upon success but sendNotification takes User type.
661+
662+
const resAsync = parseUserInput(userInput)
663+
.asyncAndThrough(insertUser)
664+
.andThen(sendNotification)
665+
666+
resAsync.then((res: Result<void, ParseError | InsertError | NotificationError>) => {e
667+
if(res.isErr()){
668+
console.log("Oops, at least one step failed", res.error)
669+
}
670+
else{
671+
console.log("User has been parsed, inserted and notified successfully.")
672+
}
673+
}))
674+
```
675+
676+
[⬆️ Back to top](#toc)
677+
678+
---
544679
#### `Result.fromThrowable` (static class method)
545680

546681
> 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.
@@ -1096,7 +1231,101 @@ const resultMessage = await validateUser(user)
10961231
[⬆️ Back to top](#toc)
10971232

10981233
---
1234+
#### `ResultAsync.andTee` (method)
1235+
1236+
Takes a `ResultAsync<T, E>` and lets the original `ResultAsync<T, E>` pass through regardless
1237+
the result of the passed-in function.
1238+
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.
1239+
1240+
**Signature:**
1241+
1242+
```typescript
1243+
class ResultAsync<T, E> {
1244+
andTee(
1245+
callback: (value: T) => unknown
1246+
): ResultAsync<T, E> => { ... }
1247+
}
1248+
```
1249+
1250+
**Example:**
1251+
1252+
```typescript
1253+
import { insertUser } from 'imaginary-database'
1254+
import { logUser } from 'imaginary-logger'
1255+
import { sendNotification } from 'imaginary-service'
1256+
1257+
// ^ assume insertUser, logUser and sendNotification have the following signatures:
1258+
// insertUser(user: User): ResultAsync<User, InsertError>
1259+
// logUser(user: User): Result<void, LogError>
1260+
// sendNotification(user: User): ResultAsync<void, NotificationError>
1261+
// Note logUser returns void on success but sendNotification takes User type.
1262+
1263+
const resAsync = insertUser(user)
1264+
.andTee(logUser)
1265+
.andThen(sendNotification)
1266+
1267+
// Note there is no LogError in the types below
1268+
resAsync.then((res: Result<void, InsertError | NotificationError>) => {e
1269+
if(res.isErr()){
1270+
console.log("Oops, at least one step failed", res.error)
1271+
}
1272+
else{
1273+
console.log("User has been inserted and notified successfully.")
1274+
}
1275+
}))
1276+
```
10991277

1278+
[⬆️ Back to top](#toc)
1279+
1280+
---
1281+
#### `ResultAsync.andThrough` (method)
1282+
1283+
1284+
Similar to `andTee` except for:
1285+
1286+
- when there is an error from the passed-in function, that error will be passed along.
1287+
1288+
**Signature:**
1289+
1290+
```typescript
1291+
class ResultAsync<T, E> {
1292+
andThrough<F>(
1293+
callback: (value: T) => Result<unknown, F> | ResultAsync<unknown, F>,
1294+
): ResultAsync<T, E | F> => { ... }
1295+
}
1296+
```
1297+
1298+
**Example:**
1299+
1300+
```typescript
1301+
1302+
import { buildUser } from 'imaginary-builder'
1303+
import { insertUser } from 'imaginary-database'
1304+
import { sendNotification } from 'imaginary-service'
1305+
1306+
// ^ assume buildUser, insertUser and sendNotification have the following signatures:
1307+
// buildUser(userRaw: UserRaw): ResultAsync<User, BuildError>
1308+
// insertUser(user: User): ResultAsync<void, InsertError>
1309+
// sendNotification(user: User): ResultAsync<void, NotificationError>
1310+
// Note insertUser returns void upon success but sendNotification takes User type.
1311+
1312+
const resAsync = buildUser(userRaw)
1313+
.andThrough(insertUser)
1314+
.andThen(sendNotification)
1315+
1316+
resAsync.then((res: Result<void, BuildError | InsertError | NotificationError>) => {e
1317+
if(res.isErr()){
1318+
console.log("Oops, at least one step failed", res.error)
1319+
}
1320+
else{
1321+
console.log("User data has been built, inserted and notified successfully.")
1322+
}
1323+
}))
1324+
```
1325+
1326+
[⬆️ Back to top](#toc)
1327+
1328+
---
11001329
#### `ResultAsync.combine` (static class method)
11011330

11021331
Combine lists of `ResultAsync`s.

src/result-async.ts

+32
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,38 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
9898
)
9999
}
100100

101+
andThrough<F>(f: (t: T) => Result<unknown, F> | ResultAsync<unknown, F>): ResultAsync<T, E | F> {
102+
return new ResultAsync(
103+
this._promise.then(async (res: Result<T, E>) => {
104+
if (res.isErr()) {
105+
return new Err<T, E>(res.error)
106+
}
107+
108+
const newRes = await f(res.value)
109+
if (newRes.isErr()) {
110+
return new Err<T, F>(newRes.error)
111+
}
112+
return new Ok<T, F>(res.value)
113+
}),
114+
)
115+
}
116+
117+
andTee(f: (t: T) => unknown): ResultAsync<T, E> {
118+
return new ResultAsync(
119+
this._promise.then(async (res: Result<T, E>) => {
120+
if (res.isErr()) {
121+
return new Err<T, E>(res.error)
122+
}
123+
try {
124+
await f(res.value)
125+
} catch (e) {
126+
// Tee does not care about the error
127+
}
128+
return new Ok<T, E>(res.value)
129+
}),
130+
)
131+
}
132+
101133
mapErr<U>(f: (e: E) => U | Promise<U>): ResultAsync<T, U> {
102134
return new ResultAsync(
103135
this._promise.then(async (res: Result<T, E>) => {

0 commit comments

Comments
 (0)