diff --git a/.changeset/thirty-avocados-explain.md b/.changeset/thirty-avocados-explain.md deleted file mode 100644 index 9d1b36ae..00000000 --- a/.changeset/thirty-avocados-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neverthrow": minor ---- - -Allow ok/err/okAsync/errAsync to accept zero arguments when returning void diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bce0d0..a6d21b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # neverthrow +## 8.2.0 + +### Minor Changes + +- [#615](https://github.com/supermacro/neverthrow/pull/615) [`85ed7fd`](https://github.com/supermacro/neverthrow/commit/85ed7fd3a1247e4c0e83bba13f5e874282243d75) Thanks [@konker](https://github.com/konker)! - Add orTee, which is the equivalent of andTee but for the error track. + +- [#584](https://github.com/supermacro/neverthrow/pull/584) [`acea44a`](https://github.com/supermacro/neverthrow/commit/acea44adb98dda2ca32fe4e882879461cc7cedc2) Thanks [@macksal](https://github.com/macksal)! - Allow ok/err/okAsync/errAsync to accept zero arguments when returning void + ## 8.1.1 ### Patch Changes diff --git a/README.md b/README.md index 7ab7a971..5777600a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`Result.match` (method)](#resultmatch-method) - [`Result.asyncMap` (method)](#resultasyncmap-method) - [`Result.andTee` (method)](#resultandtee-method) + - [`Result.orTee` (method)](#resultortee-method) - [`Result.andThrough` (method)](#resultandthrough-method) - [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method) - [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method) @@ -55,6 +56,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a - [`ResultAsync.orElse` (method)](#resultasyncorelse-method) - [`ResultAsync.match` (method)](#resultasyncmatch-method) - [`ResultAsync.andTee` (method)](#resultasyncandtee-method) + - [`ResultAsync.orTee` (method)](#resultasyncortee-method) - [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method) - [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method) - [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method) @@ -593,6 +595,53 @@ resAsync.then((res: Result) => {e --- +#### `Result.orTee` (method) + +Like `andTee` for the error track. Takes a `Result` and lets the `Err` value 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 { + orTee( + callback: (value: E) => unknown + ): Result { ... } +} +``` + +**Example:** + +```typescript +import { parseUserInput } from 'imaginary-parser' +import { logParseError } from 'imaginary-logger' +import { insertUser } from 'imaginary-database' + +// ^ assume parseUserInput, logParseError and insertUser have the following signatures: +// parseUserInput(input: RequestData): Result +// logParseError(parseError: ParseError): Result +// insertUser(user: User): ResultAsync +// Note logParseError returns void upon success but insertUser takes User type. + +const resAsync = parseUserInput(userInput) + .orTee(logParseError) + .asyncAndThen(insertUser) + +// Note no LogError shows up in the Result type +resAsync.then((res: Result) => {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: @@ -1277,6 +1326,53 @@ resAsync.then((res: Result) => {e [⬆️ Back to top](#toc) +--- +#### `ResultAsync.orTee` (method) + +Like `andTee` for the error track. Takes a `ResultAsync` and lets the original `Err` value 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 { + orTee( + callback: (value: E) => unknown + ): ResultAsync => { ... } +} +``` + +**Example:** + +```typescript +import { insertUser } from 'imaginary-database' +import { logInsertError } from 'imaginary-logger' +import { sendNotification } from 'imaginary-service' + +// ^ assume insertUser, logInsertError and sendNotification have the following signatures: +// insertUser(user: User): ResultAsync +// logInsertError(insertError: InsertError): Result +// sendNotification(user: User): ResultAsync +// Note logInsertError returns void on success but sendNotification takes User type. + +const resAsync = insertUser(user) + .orTee(logUser) + .andThen(sendNotification) + +// Note there is no LogError in the types below +resAsync.then((res: Result) => {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) diff --git a/package-lock.json b/package-lock.json index 53441ae4..bd5c0c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "neverthrow", - "version": "8.1.1", + "version": "8.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neverthrow", - "version": "8.1.1", + "version": "8.2.0", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.5.0", diff --git a/package.json b/package.json index be86c5e5..d8405bce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neverthrow", - "version": "8.1.1", + "version": "8.2.0", "description": "Stop throwing errors, and instead return Results!", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/src/result-async.ts b/src/result-async.ts index 9c349c54..20120d2b 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -130,6 +130,22 @@ export class ResultAsync implements PromiseLike> { ) } + orTee(f: (t: E) => unknown): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isOk()) { + return new Ok(res.value) + } + try { + await f(res.error) + } catch (e) { + // Tee does not care about the error + } + return new Err(res.error) + }), + ) + } + mapErr(f: (e: E) => U | Promise): ResultAsync { return new ResultAsync( this._promise.then(async (res: Result) => { diff --git a/src/result.ts b/src/result.ts index 3f6f5a94..ad447caa 100644 --- a/src/result.ts +++ b/src/result.ts @@ -194,6 +194,18 @@ interface IResult { */ andTee(f: (t: T) => unknown): Result + /** + * This "tee"s the current `Err` value to an passed-in computation such as side + * effect functions but still returns the same `Err` value as the result. + * + * This is useful when you want to pass the current `Err` value to your side-track + * work such as logging but want to continue error-track work after that. + * This method does not care about the result of the passed in computation. + * + * @param f The function to apply to the current `Err` value + */ + orTee(f: (t: E) => unknown): Result + /** * Similar to `andTee` except error result of the computation will be passed * to the downstream in case of an error. @@ -342,6 +354,10 @@ export class Ok implements IResult { return ok(this.value) } + orTee(_f: (t: E) => unknown): Result { + return ok(this.value) + } + orElse>( _f: (e: E) => R, ): Result | T, InferErrTypes> @@ -428,6 +444,15 @@ export class Err implements IResult { return err(this.error) } + orTee(f: (t: E) => unknown): Result { + try { + f(this.error) + } catch (e) { + // Tee doesn't care about the error + } + return err(this.error) + } + andThen>( _f: (t: T) => R, ): Result, InferErrTypes | E> diff --git a/tests/index.test.ts b/tests/index.test.ts index e5d94852..9f089a9d 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -159,6 +159,31 @@ describe('Result.Ok', () => { }) }) + describe('orTee', () => { + it('Calls the passed function but returns an original err', () => { + const errVal = err(12) + const passedFn = vitest.fn((_number) => {}) + + const teed = errVal.orTee(passedFn) + + expect(teed.isErr()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrapErr()).toStrictEqual(12) + }) + it('returns an original err even when the passed function fails', () => { + const errVal = err(12) + const passedFn = vitest.fn((_number) => { + throw new Error('OMG!') + }) + + const teed = errVal.orTee(passedFn) + + expect(teed.isErr()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrapErr()).toStrictEqual(12) + }) + }) + describe('asyncAndThrough', () => { it('Calls the passed function but returns an original ok as Async', async () => { const okVal = ok(12) @@ -1064,6 +1089,31 @@ describe('ResultAsync', () => { }) }) + describe('orTee', () => { + it('Calls the passed function but returns an original err', async () => { + const errVal = errAsync(12) + const passedFn = vitest.fn((_number) => {}) + + const teed = await errVal.orTee(passedFn) + + expect(teed.isErr()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrapErr()).toStrictEqual(12) + }) + it('returns an original err even when the passed function fails', async () => { + const errVal = errAsync(12) + const passedFn = vitest.fn((_number) => { + throw new Error('OMG!') + }) + + const teed = await errVal.orTee(passedFn) + + expect(teed.isErr()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrapErr()).toStrictEqual(12) + }) + }) + describe('orElse', () => { it('Skips orElse on an Ok value', async () => { const okVal = okAsync(12)