From 2d45d8ea24c942877858f034812415c9e06d0943 Mon Sep 17 00:00:00 2001 From: Yukio Mizuta Date: Sat, 21 Jan 2023 14:27:38 -0500 Subject: [PATCH 01/11] Implement andTee, andSafeTee and asyncAndTee --- src/result-async.ts | 30 +++++ src/result.ts | 31 +++++ tests/index.test.ts | 198 ++++++++++++++++++++++++++++ tests/typecheck-tests.ts | 275 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 534 insertions(+) diff --git a/src/result-async.ts b/src/result-async.ts index 87285455..fb0a515b 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -79,6 +79,36 @@ export class ResultAsync implements PromiseLike> { }), ) } + andTee(f: (t: T) => Result | ResultAsync): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isErr()) { + return new Err(res.error) + } + + const newRes = await f(res.value) + if (newRes.isErr()) { + return new Err(newRes.error) + } + return new Ok(res.value) + }) + ) + } + + // TODO: Is this a good idea? + andSafeTee(f: (t: T) => unknown | Promise): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isErr()) { + return new Err(res.error) + } + + await f(res.value) + return new Ok(res.value) + }) + ) + } + mapErr(f: (e: E) => U | Promise): ResultAsync { return new ResultAsync( diff --git a/src/result.ts b/src/result.ts index 59edf442..42345777 100644 --- a/src/result.ts +++ b/src/result.ts @@ -219,6 +219,21 @@ export class Ok implements IResult { return f(this.value) } + andTee>( + f: (t: T) => R, + ): Result | E> + andTee(f: (t: T) => Result): Result + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + andTee(f: any): any { + return f(this.value).map((_value: unknown) => this.value) + } + + // TODO: Is thiks a good idea? + andSafeTee(f: (t: T) => unknown): Result{ + f(this.value) + return ok(this.value) + } + orElse>(_f: (e: E) => R): Result> orElse(_f: (e: E) => Result): Result // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types @@ -230,6 +245,10 @@ export class Ok implements IResult { return f(this.value) } + asyncAndTee(f: (t: T) => ResultAsync): ResultAsync { + return f(this.value).map((_value: unknown) => this.value) + } + asyncMap(f: (t: T) => Promise): ResultAsync { return ResultAsync.fromSafePromise(f(this.value)) } @@ -272,6 +291,14 @@ export class Err implements IResult { mapErr(f: (e: E) => U): Result { return err(f(this.error)) } + + andTee(_f: (t: T) => Result): Result { + return err(this.error) + } + + andSafeTee(_f: (t: T) => unknown): Result{ + return err(this.error) + } andThen>( _f: (t: T) => R, @@ -294,6 +321,10 @@ export class Err implements IResult { return errAsync(this.error) } + asyncAndTee(_f: (t: T) => ResultAsync): ResultAsync { + return errAsync(this.error) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars asyncMap(_f: (t: T) => Promise): ResultAsync { return errAsync(this.error) diff --git a/tests/index.test.ts b/tests/index.test.ts index 7d2d056e..a25bf26a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -99,6 +99,84 @@ describe('Result.Ok', () => { }) }) + describe('andTee', () => { + it('Calls the passed function but returns an original ok', () => { + const okVal = ok(12) + const passedFn = jest.fn((_number) => ok(undefined)) + + const teed = okVal.andTee(passedFn) + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) + }) + + it('Maps to an Err', () => { + const okval = ok(12) + + const teed = okval.andThen((_number) => { + // ... + // complex logic + // ... + return err('Whoopsies!') + }) + + expect(teed.isOk()).toBe(false) + expect(teed._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') + + const nextFn = jest.fn((_val) => ok('noop')) + + teed.andThen(nextFn) + + expect(nextFn).not.toHaveBeenCalled() + }) + }) + + describe('andSafeTee', () => { + it('Calls the passed function but returns an original ok', () => { + const okVal = ok(12) + const passedFn = jest.fn((_number) => {}) + + const teed = okVal.andSafeTee(passedFn) + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) + }) + }) + + describe('asyncAndTee', () => { + it('Calls the passed function but returns an original ok as Async', async () => { + const okVal = ok(12) + const passedFn = jest.fn((_number) => okAsync(undefined)) + + const teedAsync = okVal.asyncAndTee(passedFn) + expect(teedAsync).toBeInstanceOf(ResultAsync) + const teed = await teedAsync + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) + }) + + it('Maps to an Err', async () => { + const okval = ok(12) + + const teedAsync = okval.asyncAndThen((_number) => { + // ... + // complex logic + // ... + return errAsync('Whoopsies!') + }) + expect(teedAsync).toBeInstanceOf(ResultAsync) + const teed = await teedAsync + expect(teed.isOk()).toBe(false) + expect(teed._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') + + const nextFn = jest.fn((_val) => ok('noop')) + + teed.andThen(nextFn) + + expect(nextFn).not.toHaveBeenCalled() + }) + }) describe('orElse', () => { it('Skips orElse on an Ok value', () => { const okVal = ok(12) @@ -242,6 +320,44 @@ describe('Result.Err', () => { expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') }) + it('Skips over andTee', () => { + const errVal = err('Yolo') + + const mapper = jest.fn((_val) => ok(undefined)) + + const hopefullyNotFlattened = errVal.andTee(mapper) + + expect(hopefullyNotFlattened.isErr()).toBe(true) + expect(mapper).not.toHaveBeenCalled() + expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') + }) + + it('Skips over andSafeTee', () => { + const errVal = err('Yolo') + + const mapper = jest.fn((_val) => {}) + + const hopefullyNotFlattened = errVal.andSafeTee(mapper) + + expect(hopefullyNotFlattened.isErr()).toBe(true) + expect(mapper).not.toHaveBeenCalled() + expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') + }) + + it('Skips over asyncAndTee but returns ResultAsync instead', async () => { + const errVal = err('Yolo') + + const mapper = jest.fn((_val) => okAsync('Async')) + + const hopefullyNotFlattened = errVal.asyncAndTee(mapper) + expect(hopefullyNotFlattened).toBeInstanceOf(ResultAsync) + + const result = await hopefullyNotFlattened + expect(result.isErr()).toBe(true) + expect(mapper).not.toHaveBeenCalled() + expect(result._unsafeUnwrapErr()).toEqual('Yolo') + }) + it('Transforms error into ResultAsync within `asyncAndThen`', async () => { const errVal = err('Yolo') @@ -826,6 +942,88 @@ describe('ResultAsync', () => { }) }) + describe('andTee', () => { + it('Returns the original value when map function returning ResultAsync succeeds', async () => { + const asyncVal = okAsync(12) + + const andTeeResultAsyncFn = jest.fn(() => okAsync('good')) + + const teed = asyncVal.andTee(andTeeResultAsyncFn) + + expect(teed).toBeInstanceOf(ResultAsync) + + const result = await teed + + expect(result.isOk()).toBe(true) + expect(result._unsafeUnwrap()).toBe(12) + expect(andTeeResultAsyncFn).toHaveBeenCalledTimes(1) + }) + + it('Maps to an error when map function returning ResultAsync fails', async () => { + const asyncVal = okAsync(12) + + const andTeeResultAsyncFn = jest.fn(() => errAsync('oh no!')) + + const teed = asyncVal.andTee(andTeeResultAsyncFn) + + expect(teed).toBeInstanceOf(ResultAsync) + + const result = await teed + + expect(result.isErr()).toBe(true) + expect(result._unsafeUnwrapErr()).toBe('oh no!') + expect(andTeeResultAsyncFn).toHaveBeenCalledTimes(1) + }) + + it('Returns the original value when map function returning Result succeeds', async () => { + const asyncVal = okAsync(12) + + const andTeeResultFn = jest.fn(() => ok('good')) + + const mapped = asyncVal.andTee(andTeeResultFn) + + expect(mapped).toBeInstanceOf(ResultAsync) + + const newVal = await mapped + + expect(newVal.isOk()).toBe(true) + expect(newVal._unsafeUnwrap()).toBe(12) + expect(andTeeResultFn).toHaveBeenCalledTimes(1) + }) + + it('Maps to an error when map function returning Result fails', async () => { + const asyncVal = okAsync(12) + + const andTeeResultFn = jest.fn(() => err('oh no!')) + + const mapped = asyncVal.andTee(andTeeResultFn) + + expect(mapped).toBeInstanceOf(ResultAsync) + + const newVal = await mapped + + expect(newVal.isErr()).toBe(true) + expect(newVal._unsafeUnwrapErr()).toBe('oh no!') + expect(andTeeResultFn).toHaveBeenCalledTimes(1) + }) + + it('Skips an Error', async () => { + const asyncVal = errAsync('Wrong format') + + const andTeeResultFn = jest.fn(() => ok('good')) + + const notMapped = asyncVal.andThen(andTeeResultFn) + + expect(notMapped).toBeInstanceOf(ResultAsync) + + const newVal = await notMapped + + expect(newVal.isErr()).toBe(true) + expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') + expect(andTeeResultFn).toHaveBeenCalledTimes(0) + }) + }) + describe('orElse', () => { it('Skips orElse on an Ok value', async () => { const okVal = okAsync(12) diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index 8fd7ce70..4b35b2b4 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -142,6 +142,132 @@ import { Transpose } from '../src/result' }); }); + (function describe(_ = 'andTee') { + (function it(_ = 'Combines two equal error types (native scalar types)') { + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => err('yoooooo dude' + val)) + }); + + (function it(_ = 'Combines two equal error types (custom types)') { + interface MyError { + stack: string + code: number + } + + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => err({ stack: '/blah', code: 500 })) + }); + + (function it(_ = 'Creates a union of error types for disjoint types') { + interface MyError { + stack: string + code: number + } + + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => err(['oh nooooo'])) + }); + + (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + case 2: + return err(123) + default: + return err(false) + } + }) + }); + + (function it(_ = 'Infers error type when returning disjoint types (custom types)') { + interface MyError { + stack: string + code: number + } + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + case 2: + return err(123) + default: + return err({ stack: '/blah', code: 500 }) + } + }) + }); + + (function it(_ = 'Returns the original ok type when returning both Ok and Err (same as initial)') { + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + default: + return ok(val + 456) + } + }) + }); + + (function it(_ = 'Returns the original ok type when returning both Ok and Err (different from initial)') { + const initial = ok(123) + type Expectation = Result + + const result: Expectation = initial + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + default: + return ok("Hi" + val) + } + }) + }); + + (function it(_ = 'Infers new err type when returning both Ok and Err') { + interface MyError { + stack: string + code: number + } + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + case 2: + return ok(123) + default: + return err({ stack: '/blah', code: 500 }) + } + }) + }); + + (function it(_ = 'allows specifying the E type explicitly') { + type Expectation = Result + + const result: Expectation = ok(123).andTee(val => { + return ok('yo') + }) + }); + }); + (function describe(_ = 'orElse') { (function it(_ = 'the type of the argument is the error type of the result') { type Expectation = string @@ -244,6 +370,155 @@ import { Transpose } from '../src/result' }); }); + (function describe(_ = 'asyncAndTee') { + (function it(_ = 'Combines two equal error types (native scalar types)') { + type Expectation = ResultAsync + + const result: Expectation = ok(123) + .asyncAndTee((val) => errAsync('yoooooo dude' + val)) + }); + + (function it(_ = 'Combines two equal error types (custom types)') { + interface MyError { + stack: string + code: number + } + + type Expectation = ResultAsync + + const result: Expectation = ok(123) + .asyncAndTee((val) => errAsync({ stack: '/blah', code: 500 })) + }); + + (function it(_ = 'Creates a union of error types for disjoint types') { + interface MyError { + stack: string + code: number + } + + type Expectation = ResultAsync + + const result: Expectation = ok(123) + .asyncAndTee((val) => errAsync(['oh nooooo'])) + }); + + // (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { + // type Expectation = ResultAsync + + // const result: Expectation = ok(123) + // .asyncAndTee((val) => { + // switch (val) { + // case 1: + // return errAsync('yoooooo dude' + val) + // case 2: + // return errAsync(123) + // default: + // return errAsync(false) + // } + // }) + // }); + + (function it(_ = 'Infers error type when returning disjoint types (custom types)') { + interface MyError { + stack: string + code: number + } + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + case 2: + return err(123) + default: + return err({ stack: '/blah', code: 500 }) + } + }) + }); + + (function it(_ = 'Returns the original ok type when returning both Ok and Err (same as initial)') { + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + default: + return ok(val + 456) + } + }) + }); + + (function it(_ = 'Returns the original ok type when returning both Ok and Err (different from initial)') { + const initial = ok(123) + type Expectation = Result + + const result: Expectation = initial + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + default: + return ok("Hi" + val) + } + }) + }); + + (function it(_ = 'Infers new err type when returning both Ok and Err') { + interface MyError { + stack: string + code: number + } + type Expectation = Result + + const result: Expectation = ok(123) + .andTee((val) => { + switch (val) { + case 1: + return err('yoooooo dude' + val) + case 2: + return ok(123) + default: + return err({ stack: '/blah', code: 500 }) + } + }) + }); + + (function it(_ = 'allows specifying the E type explicitly') { + type Expectation = Result + + const result: Expectation = ok(123).andTee(val => { + return ok('yo') + }) + }); + + + (function it(_ = 'Infers new err type when returning both Ok and Err') { + interface MyError { + stack: string + code: number + } + type Expectation = ResultAsync + + const result: Expectation = ok(123) + .asyncAndTee((val) => { + switch (val) { + case 1: + return errAsync('yoooooo dude' + val) + case 2: + return okAsync(123) + default: + return errAsync({ stack: '/blah', code: 500 }) + } + }) + }); + + + }); + (function describe(_ = 'combine') { (function it(_ = 'combines different results into one') { type Expectation = Result<[ number, string, boolean, boolean ], Error | string | string[]>; From 39b85719672bf6932a39e1c052454f194787b23c Mon Sep 17 00:00:00 2001 From: Yukio Mizuta Date: Sun, 2 Apr 2023 21:17:31 -0400 Subject: [PATCH 02/11] Change andTee to andThrough, andSafeTee to andTee upon issue suggestion --- src/result-async.ts | 22 +++---- src/result.ts | 51 +++++++++++----- tests/index.test.ts | 123 ++++++++++++++++++++++++++------------- tests/typecheck-tests.ts | 42 ++++++------- 4 files changed, 152 insertions(+), 86 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index fb0a515b..988d99ce 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -79,9 +79,10 @@ export class ResultAsync implements PromiseLike> { }), ) } - andTee(f: (t: T) => Result | ResultAsync): ResultAsync { + + andThrough(f: (t: T) => Result | ResultAsync): ResultAsync { return new ResultAsync( - this._promise.then(async (res: Result) => { + this._promise.then(async (res: Result) => { if (res.isErr()) { return new Err(res.error) } @@ -91,25 +92,26 @@ export class ResultAsync implements PromiseLike> { return new Err(newRes.error) } return new Ok(res.value) - }) + }), ) } - // TODO: Is this a good idea? - andSafeTee(f: (t: T) => unknown | Promise): ResultAsync { + andTee(f: (t: T) => unknown): ResultAsync { return new ResultAsync( - this._promise.then(async (res: Result) => { + this._promise.then(async (res: Result) => { if (res.isErr()) { return new Err(res.error) } - - await f(res.value) + try { + await f(res.value) + } catch (e) { + // Tee does not care about the error + } return new Ok(res.value) - }) + }), ) } - 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 42345777..95109ab3 100644 --- a/src/result.ts +++ b/src/result.ts @@ -115,6 +115,30 @@ interface IResult { ): Result, InferErrTypes | E> andThen(f: (t: T) => Result): Result + /** + * This "tee"s the current value to an passed-in computation such as side + * effect functions but still returns the same current value as the result. + * + * This is useful when you want to pass the current result to your side-track + * work such as logging but want to continue main-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 value + */ + andTee(f: (t: T) => unknown): Result + + /** + * Similar to `andTee` except error result of the computation will be passed + * to the downstream in case of an error. + * + * This version is useful when you want to make side-effects but in case of an + * error, you want to pass the error to the downstream. + * + * @param f The function to apply to the current value + */ + andThrough>(f: (t: T) => R): Result | E> + andThrough(f: (t: T) => Result): Result + /** * Takes an `Err` value and maps it to a `Result`. * @@ -219,18 +243,19 @@ export class Ok implements IResult { return f(this.value) } - andTee>( - f: (t: T) => R, - ): Result | E> - andTee(f: (t: T) => Result): Result + andThrough>(f: (t: T) => R): Result | E> + andThrough(f: (t: T) => Result): Result // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - andTee(f: any): any { + andThrough(f: any): any { return f(this.value).map((_value: unknown) => this.value) } - // TODO: Is thiks a good idea? - andSafeTee(f: (t: T) => unknown): Result{ - f(this.value) + andTee(f: (t: T) => unknown): Result { + try { + f(this.value) + } catch (e) { + // Tee doesn't care about the error + } return ok(this.value) } @@ -245,7 +270,7 @@ export class Ok implements IResult { return f(this.value) } - asyncAndTee(f: (t: T) => ResultAsync): ResultAsync { + asyncAndThrough(f: (t: T) => ResultAsync): ResultAsync { return f(this.value).map((_value: unknown) => this.value) } @@ -291,12 +316,12 @@ export class Err implements IResult { mapErr(f: (e: E) => U): Result { return err(f(this.error)) } - - andTee(_f: (t: T) => Result): Result { + + andThrough(_f: (t: T) => Result): Result { return err(this.error) } - andSafeTee(_f: (t: T) => unknown): Result{ + andTee(_f: (t: T) => unknown): Result { return err(this.error) } @@ -321,7 +346,7 @@ export class Err implements IResult { return errAsync(this.error) } - asyncAndTee(_f: (t: T) => ResultAsync): ResultAsync { + asyncAndThrough(_f: (t: T) => ResultAsync): ResultAsync { return errAsync(this.error) } diff --git a/tests/index.test.ts b/tests/index.test.ts index a25bf26a..6b27d92a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -99,56 +99,67 @@ describe('Result.Ok', () => { }) }) - describe('andTee', () => { + describe('andThrough', () => { it('Calls the passed function but returns an original ok', () => { const okVal = ok(12) const passedFn = jest.fn((_number) => ok(undefined)) - const teed = okVal.andTee(passedFn) - expect(teed.isOk()).toBe(true) + const thrued = okVal.andThrough(passedFn) + expect(thrued.isOk()).toBe(true) expect(passedFn).toHaveBeenCalledTimes(1) - expect(teed._unsafeUnwrap()).toStrictEqual(12) + expect(thrued._unsafeUnwrap()).toStrictEqual(12) }) it('Maps to an Err', () => { const okval = ok(12) - const teed = okval.andThen((_number) => { + const thrued = okval.andThen((_number) => { // ... // complex logic // ... return err('Whoopsies!') }) - expect(teed.isOk()).toBe(false) - expect(teed._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') + expect(thrued.isOk()).toBe(false) + expect(thrued._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') const nextFn = jest.fn((_val) => ok('noop')) - teed.andThen(nextFn) + thrued.andThen(nextFn) expect(nextFn).not.toHaveBeenCalled() }) }) - describe('andSafeTee', () => { + describe('andTee', () => { it('Calls the passed function but returns an original ok', () => { const okVal = ok(12) const passedFn = jest.fn((_number) => {}) - const teed = okVal.andSafeTee(passedFn) + const teed = okVal.andTee(passedFn) + + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) + }) + it('returns an original ok even when the passed function fails', () => { + const okVal = ok(12) + const passedFn = jest.fn((_number) => { throw new Error('OMG!') }) + + const teed = okVal.andTee(passedFn) + expect(teed.isOk()).toBe(true) expect(passedFn).toHaveBeenCalledTimes(1) expect(teed._unsafeUnwrap()).toStrictEqual(12) }) }) - describe('asyncAndTee', () => { + describe('asyncAndThrough', () => { it('Calls the passed function but returns an original ok as Async', async () => { const okVal = ok(12) const passedFn = jest.fn((_number) => okAsync(undefined)) - const teedAsync = okVal.asyncAndTee(passedFn) + const teedAsync = okVal.asyncAndThrough(passedFn) expect(teedAsync).toBeInstanceOf(ResultAsync) const teed = await teedAsync expect(teed.isOk()).toBe(true) @@ -320,36 +331,36 @@ describe('Result.Err', () => { expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') }) - it('Skips over andTee', () => { + it('Skips over andThrough', () => { const errVal = err('Yolo') const mapper = jest.fn((_val) => ok(undefined)) - const hopefullyNotFlattened = errVal.andTee(mapper) + const hopefullyNotFlattened = errVal.andThrough(mapper) expect(hopefullyNotFlattened.isErr()).toBe(true) expect(mapper).not.toHaveBeenCalled() expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') }) - it('Skips over andSafeTee', () => { + it('Skips over andTee', () => { const errVal = err('Yolo') const mapper = jest.fn((_val) => {}) - const hopefullyNotFlattened = errVal.andSafeTee(mapper) + const hopefullyNotFlattened = errVal.andTee(mapper) expect(hopefullyNotFlattened.isErr()).toBe(true) expect(mapper).not.toHaveBeenCalled() expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') }) - it('Skips over asyncAndTee but returns ResultAsync instead', async () => { + it('Skips over asyncAndThrough but returns ResultAsync instead', async () => { const errVal = err('Yolo') const mapper = jest.fn((_val) => okAsync('Async')) - const hopefullyNotFlattened = errVal.asyncAndTee(mapper) + const hopefullyNotFlattened = errVal.asyncAndThrough(mapper) expect(hopefullyNotFlattened).toBeInstanceOf(ResultAsync) const result = await hopefullyNotFlattened @@ -942,77 +953,82 @@ describe('ResultAsync', () => { }) }) - describe('andTee', () => { + describe('andThrough', () => { it('Returns the original value when map function returning ResultAsync succeeds', async () => { const asyncVal = okAsync(12) + /* + A couple examples of this function - const andTeeResultAsyncFn = jest.fn(() => okAsync('good')) + DB persistence (create or update) + API calls (create or update) + */ + const andThroughResultAsyncFn = jest.fn(() => okAsync('good')) - const teed = asyncVal.andTee(andTeeResultAsyncFn) + const thrued = asyncVal.andThrough(andThroughResultAsyncFn) - expect(teed).toBeInstanceOf(ResultAsync) + expect(thrued).toBeInstanceOf(ResultAsync) - const result = await teed + const result = await thrued expect(result.isOk()).toBe(true) expect(result._unsafeUnwrap()).toBe(12) - expect(andTeeResultAsyncFn).toHaveBeenCalledTimes(1) + expect(andThroughResultAsyncFn).toHaveBeenCalledTimes(1) }) it('Maps to an error when map function returning ResultAsync fails', async () => { const asyncVal = okAsync(12) - const andTeeResultAsyncFn = jest.fn(() => errAsync('oh no!')) + const andThroughResultAsyncFn = jest.fn(() => errAsync('oh no!')) - const teed = asyncVal.andTee(andTeeResultAsyncFn) + const thrued = asyncVal.andThrough(andThroughResultAsyncFn) - expect(teed).toBeInstanceOf(ResultAsync) + expect(thrued).toBeInstanceOf(ResultAsync) - const result = await teed + const result = await thrued expect(result.isErr()).toBe(true) expect(result._unsafeUnwrapErr()).toBe('oh no!') - expect(andTeeResultAsyncFn).toHaveBeenCalledTimes(1) + expect(andThroughResultAsyncFn).toHaveBeenCalledTimes(1) }) it('Returns the original value when map function returning Result succeeds', async () => { const asyncVal = okAsync(12) - const andTeeResultFn = jest.fn(() => ok('good')) + const andThroughResultFn = jest.fn(() => ok('good')) - const mapped = asyncVal.andTee(andTeeResultFn) + const thrued = asyncVal.andThrough(andThroughResultFn) - expect(mapped).toBeInstanceOf(ResultAsync) + expect(thrued).toBeInstanceOf(ResultAsync) - const newVal = await mapped + const newVal = await thrued expect(newVal.isOk()).toBe(true) expect(newVal._unsafeUnwrap()).toBe(12) - expect(andTeeResultFn).toHaveBeenCalledTimes(1) + expect(andThroughResultFn).toHaveBeenCalledTimes(1) }) it('Maps to an error when map function returning Result fails', async () => { const asyncVal = okAsync(12) - const andTeeResultFn = jest.fn(() => err('oh no!')) + const andThroughResultFn = jest.fn(() => err('oh no!')) - const mapped = asyncVal.andTee(andTeeResultFn) + const thrued = asyncVal.andThrough(andThroughResultFn) - expect(mapped).toBeInstanceOf(ResultAsync) + expect(thrued).toBeInstanceOf(ResultAsync) - const newVal = await mapped + const newVal = await thrued expect(newVal.isErr()).toBe(true) expect(newVal._unsafeUnwrapErr()).toBe('oh no!') - expect(andTeeResultFn).toHaveBeenCalledTimes(1) + expect(andThroughResultFn).toHaveBeenCalledTimes(1) }) it('Skips an Error', async () => { const asyncVal = errAsync('Wrong format') - const andTeeResultFn = jest.fn(() => ok('good')) + const andThroughResultFn = jest.fn(() => ok('good')) - const notMapped = asyncVal.andThen(andTeeResultFn) + const notMapped = asyncVal.andThen(andThroughResultFn) expect(notMapped).toBeInstanceOf(ResultAsync) @@ -1020,7 +1036,30 @@ describe('ResultAsync', () => { expect(newVal.isErr()).toBe(true) expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') - expect(andTeeResultFn).toHaveBeenCalledTimes(0) + expect(andThroughResultFn).toHaveBeenCalledTimes(0) + }) + }) + + describe('andTee', () => { + it('Calls the passed function but returns an original ok', async () => { + const okVal = okAsync(12) + const passedFn = jest.fn((_number) => {}) + + const teed = await okVal.andTee(passedFn) + + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) + }) + it('returns an original ok even when the passed function fails', async () => { + const okVal = okAsync(12) + const passedFn = jest.fn((_number) => { throw new Error('OMG!') }) + + const teed = await okVal.andTee(passedFn) + + expect(teed.isOk()).toBe(true) + expect(passedFn).toHaveBeenCalledTimes(1) + expect(teed._unsafeUnwrap()).toStrictEqual(12) }) }) diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index 4b35b2b4..a0400f97 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -142,12 +142,12 @@ import { Transpose } from '../src/result' }); }); - (function describe(_ = 'andTee') { + (function describe(_ = 'andThrough') { (function it(_ = 'Combines two equal error types (native scalar types)') { type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => err('yoooooo dude' + val)) + .andThrough((val) => err('yoooooo dude' + val)) }); (function it(_ = 'Combines two equal error types (custom types)') { @@ -159,7 +159,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => err({ stack: '/blah', code: 500 })) + .andThrough((val) => err({ stack: '/blah', code: 500 })) }); (function it(_ = 'Creates a union of error types for disjoint types') { @@ -171,14 +171,14 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => err(['oh nooooo'])) + .andThrough((val) => err(['oh nooooo'])) }); (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -198,7 +198,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -214,7 +214,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -229,7 +229,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = initial - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -247,7 +247,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -262,7 +262,7 @@ import { Transpose } from '../src/result' (function it(_ = 'allows specifying the E type explicitly') { type Expectation = Result - const result: Expectation = ok(123).andTee(val => { + const result: Expectation = ok(123).andThrough(val => { return ok('yo') }) }); @@ -370,12 +370,12 @@ import { Transpose } from '../src/result' }); }); - (function describe(_ = 'asyncAndTee') { + (function describe(_ = 'asyncAndThrough') { (function it(_ = 'Combines two equal error types (native scalar types)') { type Expectation = ResultAsync const result: Expectation = ok(123) - .asyncAndTee((val) => errAsync('yoooooo dude' + val)) + .asyncAndThrough((val) => errAsync('yoooooo dude' + val)) }); (function it(_ = 'Combines two equal error types (custom types)') { @@ -387,7 +387,7 @@ import { Transpose } from '../src/result' type Expectation = ResultAsync const result: Expectation = ok(123) - .asyncAndTee((val) => errAsync({ stack: '/blah', code: 500 })) + .asyncAndThrough((val) => errAsync({ stack: '/blah', code: 500 })) }); (function it(_ = 'Creates a union of error types for disjoint types') { @@ -399,14 +399,14 @@ import { Transpose } from '../src/result' type Expectation = ResultAsync const result: Expectation = ok(123) - .asyncAndTee((val) => errAsync(['oh nooooo'])) + .asyncAndThrough((val) => errAsync(['oh nooooo'])) }); // (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { // type Expectation = ResultAsync // const result: Expectation = ok(123) - // .asyncAndTee((val) => { + // .asyncAndThrough((val) => { // switch (val) { // case 1: // return errAsync('yoooooo dude' + val) @@ -426,7 +426,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -442,7 +442,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -457,7 +457,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = initial - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -475,7 +475,7 @@ import { Transpose } from '../src/result' type Expectation = Result const result: Expectation = ok(123) - .andTee((val) => { + .andThrough((val) => { switch (val) { case 1: return err('yoooooo dude' + val) @@ -490,7 +490,7 @@ import { Transpose } from '../src/result' (function it(_ = 'allows specifying the E type explicitly') { type Expectation = Result - const result: Expectation = ok(123).andTee(val => { + const result: Expectation = ok(123).andThrough(val => { return ok('yo') }) }); @@ -504,7 +504,7 @@ import { Transpose } from '../src/result' type Expectation = ResultAsync const result: Expectation = ok(123) - .asyncAndTee((val) => { + .asyncAndThrough((val) => { switch (val) { case 1: return errAsync('yoooooo dude' + val) From f4a91ce1785d64ba0c814dc016381d73d5df62ec Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Tue, 27 Jun 2023 12:38:13 +0200 Subject: [PATCH 03/11] Fix combineWithAllErrors types --- src/result-async.ts | 21 +++++------------- src/result.ts | 8 +++---- tests/typecheck-tests.ts | 48 ++++++++++++++++++++++++++++------------ 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index 87285455..be685d05 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -219,22 +219,13 @@ type TraverseAsync = IsLiteralArray 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 = IsLiteralArray extends 1 - ? Combine extends [infer Oks, infer Errs] - ? ResultAsync, EmptyArrayToNever> - : never - : Writable extends Array - ? Combine, Depth> extends [infer Oks, infer Errs] - ? Oks extends unknown[] - ? Errs extends unknown[] - ? ResultAsync, EmptyArrayToNever> - : ResultAsync, Errs> - : Errs extends unknown[] - ? ResultAsync> - : ResultAsync - : never +type TraverseWithAllErrorsAsync = TraverseAsync< + T, + Depth +> extends ResultAsync + ? ResultAsync : never // Converts a reaodnly array into a writable array diff --git a/src/result.ts b/src/result.ts index c8f09971..8602fc24 100644 --- a/src/result.ts +++ b/src/result.ts @@ -497,11 +497,11 @@ type Traverse = Combine extends [infer Ok // Traverses an array of results and returns a single result containing // the oks combined and the array of errors combined. -type TraverseWithAllErrors = Combine extends [ +type TraverseWithAllErrors = Traverse extends Result< infer Oks, - infer Errs, -] - ? Result, EmptyArrayToNever> + infer Errs +> + ? Result : never // Combines the array of results into one result. diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index 849b2247..a38a90e4 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -854,7 +854,7 @@ type CreateTuple = (function describe(_ = 'combineWithAllErrors') { (function it(_ = 'combines different results into one') { - type Expectation = Result<[ number, string, never, never ], [never, never, string[], Error]>; + type Expectation = Result<[ number, string, never, never ], [string[] | Error, ...(string[] | Error)[]]>; const result = Result.combineWithAllErrors([ ok(1), @@ -868,7 +868,7 @@ type CreateTuple = }); (function it(_ = 'combines only ok results into one') { - type Expectation = Result<[ number, string ], [never, never]>; + type Expectation = Result<[ number, string ], never>; const result = Result.combineWithAllErrors([ ok(1), @@ -880,7 +880,7 @@ type CreateTuple = }); (function it(_ = 'combines only err results into one') { - type Expectation = Result<[ never, never ], [number, string]>; + type Expectation = Result<[ never, never ], [number | string, ...(number | string)[]]>; const result = Result.combineWithAllErrors([ err(1), @@ -901,10 +901,20 @@ type CreateTuple = const assignablefromCheck: typeof result = assignableToCheck; }); + (function it(_ = 'combines arrays of different results to a result of an array') { + type Expectation = Result<(string | boolean)[], (number | string)[]>; + const results: (Result | Result)[] = []; + + const result = Result.combineWithAllErrors(results); + + const assignableToCheck: Expectation = result; + const assignablefromCheck: typeof result = assignableToCheck; + }); + (function describe(_ = 'inference on large tuples') { (function it(_ = 'Should correctly infer the type on tuples with 6 elements') { type Input = CreateTuple<6, Result> - type Expectation = Result, CreateTuple<6, number>> + type Expectation = Result, [number, ...number[]]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -916,7 +926,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 15 elements') { type Input = CreateTuple<15, Result> - type Expectation = Result, CreateTuple<15, number>> + type Expectation = Result, [number, ...number[]]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -928,7 +938,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 30 elements') { type Input = CreateTuple<30, Result> - type Expectation = Result, CreateTuple<30, number>> + type Expectation = Result, [number, ...number[]]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -940,7 +950,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 49 elements') { type Input = CreateTuple<49 , Result> - type Expectation = Result, CreateTuple<49, number>> + type Expectation = Result, [number, ...number[]]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -1858,7 +1868,7 @@ type CreateTuple = (function describe(_ = 'combineWithAllErrors') { (function it(_ = 'combines different result asyncs into one') { - type Expectation = ResultAsync<[ number, string, never, never ], [never, never, string[], Error]>; + type Expectation = ResultAsync<[ number, string, never, never ], [string[] | Error, ...(string[] | Error)[]]>; const result = ResultAsync.combineWithAllErrors([ okAsync(1), @@ -1872,7 +1882,7 @@ type CreateTuple = }); (function it(_ = 'combines only ok result asyncs into one') { - type Expectation = ResultAsync<[ number, string ], [never, never]>; + type Expectation = ResultAsync<[ number, string ], never>; const result = ResultAsync.combineWithAllErrors([ okAsync(1), @@ -1884,7 +1894,7 @@ type CreateTuple = }); (function it(_ = 'combines only err result asyncs into one') { - type Expectation = ResultAsync<[ never, never ], [number, string]>; + type Expectation = ResultAsync<[ never, never ], [number | string, ...(number | string)[]]>; const result = ResultAsync.combineWithAllErrors([ errAsync(1), @@ -1905,10 +1915,20 @@ type CreateTuple = const assignablefromCheck: typeof result = assignableToCheck; }); + (function it(_ = 'combines arrays of different result asyncs to a result of an array') { + type Expectation = ResultAsync<(string | boolean)[], (number | string)[]>; + const results: (ResultAsync | ResultAsync)[] = []; + + const result = ResultAsync.combineWithAllErrors(results); + + const assignableToCheck: Expectation = result; + const assignablefromCheck: typeof result = assignableToCheck; + }); + (function describe(_ = 'inference on large tuples') { (function it(_ = 'Should correctly infer the type on tuples with 6 elements') { type Input = CreateTuple<6, ResultAsync> - type Expectation = ResultAsync, CreateTuple<6, number>> + type Expectation = ResultAsync, [number, ...number[]]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1920,7 +1940,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 15 elements') { type Input = CreateTuple<15, ResultAsync> - type Expectation = ResultAsync, CreateTuple<15, number>> + type Expectation = ResultAsync, [number, ...number[]]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1932,7 +1952,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 30 elements') { type Input = CreateTuple<30, ResultAsync> - type Expectation = ResultAsync, CreateTuple<30, number>> + type Expectation = ResultAsync, [number, ...number[]]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1944,7 +1964,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 49 elements') { type Input = CreateTuple<49 , ResultAsync> - type Expectation = ResultAsync, CreateTuple<49, number>> + type Expectation = ResultAsync, [number, ...number[]]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) From fa4bbb1b908cb256caaca630d5ee2ad0602fb538 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Tue, 27 Jun 2023 12:51:29 +0200 Subject: [PATCH 04/11] Remove non-empty type guarantee just to simplify --- src/result-async.ts | 2 +- src/result.ts | 2 +- tests/typecheck-tests.ts | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index be685d05..904b8e36 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -225,7 +225,7 @@ type TraverseWithAllErrorsAsync = TraverseAsync< T, Depth > extends ResultAsync - ? ResultAsync + ? ResultAsync : never // Converts a reaodnly array into a writable array diff --git a/src/result.ts b/src/result.ts index 8602fc24..c5770bd3 100644 --- a/src/result.ts +++ b/src/result.ts @@ -501,7 +501,7 @@ type TraverseWithAllErrors = Traverse ext infer Oks, infer Errs > - ? Result + ? Result : never // Combines the array of results into one result. diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index a38a90e4..5116fc62 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -854,7 +854,7 @@ type CreateTuple = (function describe(_ = 'combineWithAllErrors') { (function it(_ = 'combines different results into one') { - type Expectation = Result<[ number, string, never, never ], [string[] | Error, ...(string[] | Error)[]]>; + type Expectation = Result<[ number, string, never, never ], (string[] | Error)[]>; const result = Result.combineWithAllErrors([ ok(1), @@ -868,7 +868,7 @@ type CreateTuple = }); (function it(_ = 'combines only ok results into one') { - type Expectation = Result<[ number, string ], never>; + type Expectation = Result<[ number, string ], never[]>; const result = Result.combineWithAllErrors([ ok(1), @@ -880,7 +880,7 @@ type CreateTuple = }); (function it(_ = 'combines only err results into one') { - type Expectation = Result<[ never, never ], [number | string, ...(number | string)[]]>; + type Expectation = Result<[ never, never ], (number | string)[]>; const result = Result.combineWithAllErrors([ err(1), @@ -914,7 +914,7 @@ type CreateTuple = (function describe(_ = 'inference on large tuples') { (function it(_ = 'Should correctly infer the type on tuples with 6 elements') { type Input = CreateTuple<6, Result> - type Expectation = Result, [number, ...number[]]> + type Expectation = Result, number[]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -926,7 +926,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 15 elements') { type Input = CreateTuple<15, Result> - type Expectation = Result, [number, ...number[]]> + type Expectation = Result, number[]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -938,7 +938,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 30 elements') { type Input = CreateTuple<30, Result> - type Expectation = Result, [number, ...number[]]> + type Expectation = Result, number[]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -950,7 +950,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 49 elements') { type Input = CreateTuple<49 , Result> - type Expectation = Result, [number, ...number[]]> + type Expectation = Result, number[]> const inputValues = input() const result = Result.combineWithAllErrors(inputValues) @@ -1868,7 +1868,7 @@ type CreateTuple = (function describe(_ = 'combineWithAllErrors') { (function it(_ = 'combines different result asyncs into one') { - type Expectation = ResultAsync<[ number, string, never, never ], [string[] | Error, ...(string[] | Error)[]]>; + type Expectation = ResultAsync<[ number, string, never, never ], (string[] | Error)[]>; const result = ResultAsync.combineWithAllErrors([ okAsync(1), @@ -1882,7 +1882,7 @@ type CreateTuple = }); (function it(_ = 'combines only ok result asyncs into one') { - type Expectation = ResultAsync<[ number, string ], never>; + type Expectation = ResultAsync<[ number, string ], never[]>; const result = ResultAsync.combineWithAllErrors([ okAsync(1), @@ -1894,7 +1894,7 @@ type CreateTuple = }); (function it(_ = 'combines only err result asyncs into one') { - type Expectation = ResultAsync<[ never, never ], [number | string, ...(number | string)[]]>; + type Expectation = ResultAsync<[ never, never ], (number | string)[]>; const result = ResultAsync.combineWithAllErrors([ errAsync(1), @@ -1928,7 +1928,7 @@ type CreateTuple = (function describe(_ = 'inference on large tuples') { (function it(_ = 'Should correctly infer the type on tuples with 6 elements') { type Input = CreateTuple<6, ResultAsync> - type Expectation = ResultAsync, [number, ...number[]]> + type Expectation = ResultAsync, number[]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1940,7 +1940,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 15 elements') { type Input = CreateTuple<15, ResultAsync> - type Expectation = ResultAsync, [number, ...number[]]> + type Expectation = ResultAsync, number[]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1952,7 +1952,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 30 elements') { type Input = CreateTuple<30, ResultAsync> - type Expectation = ResultAsync, [number, ...number[]]> + type Expectation = ResultAsync, number[]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) @@ -1964,7 +1964,7 @@ type CreateTuple = (function it(_ = 'Should correctly infer the type on tuples with 49 elements') { type Input = CreateTuple<49 , ResultAsync> - type Expectation = ResultAsync, [number, ...number[]]> + type Expectation = ResultAsync, number[]> const inputValues = input() const result = ResultAsync.combineWithAllErrors(inputValues) From aeb3cdf6c8b043bfdca63ab159464e0a0d082920 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Tue, 27 Jun 2023 13:06:41 +0200 Subject: [PATCH 05/11] Fix the tests --- tests/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/index.test.ts b/tests/index.test.ts index 7d2d056e..332d7bc1 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -583,7 +583,7 @@ describe('Utils', () => { okAsync(true), ] - type ExpecteResult = Result<[ string, number, boolean ], [string, number, boolean]> + type ExpecteResult = Result<[ string, number, boolean ], (string | number | boolean)[]> const result: ExpecteResult = await ResultAsync.combineWithAllErrors(heterogenousList) From 96f7f669ac83be705a389d47ed804e9d44a13932 Mon Sep 17 00:00:00 2001 From: m-shaka Date: Wed, 21 Aug 2024 10:29:48 +0900 Subject: [PATCH 06/11] Create thirty-grapes-drum.md --- .changeset/thirty-grapes-drum.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-grapes-drum.md diff --git a/.changeset/thirty-grapes-drum.md b/.changeset/thirty-grapes-drum.md new file mode 100644 index 00000000..10867d68 --- /dev/null +++ b/.changeset/thirty-grapes-drum.md @@ -0,0 +1,5 @@ +--- +"neverthrow": patch +--- + +Fix `combineWithAllErrors` types From 98d0abc58b3b7159d784bbb539678a44677355c2 Mon Sep 17 00:00:00 2001 From: Yukio Mizuta Date: Sun, 25 Aug 2024 18:18:55 -0500 Subject: [PATCH 07/11] Update per suggestions --- src/result.ts | 12 +++++++++--- tests/index.test.ts | 2 +- tests/typecheck-tests.ts | 30 +++++++++++++++--------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/result.ts b/src/result.ts index 6c92e2ef..3a00d901 100644 --- a/src/result.ts +++ b/src/result.ts @@ -5,6 +5,7 @@ import { combineResultListWithAllErrors, ExtractErrTypes, ExtractOkTypes, + InferAsyncErrTypes, InferErrTypes, InferOkTypes, } from './_internals/utils' @@ -343,8 +344,13 @@ export class Ok implements IResult { return f(this.value) } - asyncAndThrough(f: (t: T) => ResultAsync): ResultAsync { - return f(this.value).map((_value: unknown) => this.value) + asyncAndThrough>( + f: (t: T) => R, + ): ResultAsync | E> + asyncAndThrough(f: (t: T) => ResultAsync): ResultAsync + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + asyncAndThrough(f: (t: T) => ResultAsync): any { + return f(this.value).map(() => this.value) } asyncMap(f: (t: T) => Promise): ResultAsync { @@ -427,7 +433,7 @@ export class Err implements IResult { return errAsync(this.error) } - asyncAndThrough(_f: (t: T) => ResultAsync): ResultAsync { + asyncAndThrough(_f: (t: T) => ResultAsync): ResultAsync { return errAsync(this.error) } diff --git a/tests/index.test.ts b/tests/index.test.ts index 955c35ae..81f17157 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1029,7 +1029,7 @@ describe('ResultAsync', () => { const andThroughResultFn = jest.fn(() => ok('good')) - const notMapped = asyncVal.andThen(andThroughResultFn) + const notMapped = asyncVal.andThrough(andThroughResultFn) expect(notMapped).toBeInstanceOf(ResultAsync) diff --git a/tests/typecheck-tests.ts b/tests/typecheck-tests.ts index 86dbc37c..28b9c1b1 100644 --- a/tests/typecheck-tests.ts +++ b/tests/typecheck-tests.ts @@ -468,21 +468,21 @@ type CreateTuple = .asyncAndThrough((val) => errAsync(['oh nooooo'])) }); - // (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { - // type Expectation = ResultAsync - - // const result: Expectation = ok(123) - // .asyncAndThrough((val) => { - // switch (val) { - // case 1: - // return errAsync('yoooooo dude' + val) - // case 2: - // return errAsync(123) - // default: - // return errAsync(false) - // } - // }) - // }); + (function it(_ = 'Infers error type when returning disjoint types (native scalar types)') { + type Expectation = ResultAsync + + const result: Expectation = ok(123) + .asyncAndThrough((val) => { + switch (val) { + case 1: + return errAsync('yoooooo dude' + val) + case 2: + return errAsync(123) + default: + return errAsync(false) + } + }) + }); (function it(_ = 'Infers error type when returning disjoint types (custom types)') { interface MyError { From 4b9d2fdaf03223945068509f948b57194732aa03 Mon Sep 17 00:00:00 2001 From: Yukio Mizuta Date: Tue, 27 Aug 2024 22:34:18 -0500 Subject: [PATCH 08/11] Update README.md with new .andTee(), .andThrough() and asyncAndThrough() --- README.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/README.md b/README.md index d139360d..19414d50 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) @@ -541,6 +546,136 @@ Note that in the above example if `parseHeaders` returns an `Err` then `.map` an --- +#### `Result.andTee` (method) + +Takes a `Result` and lets the original `Result` 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 { + andTee( + callback: (value: T) => unknown + ): Result { ... } +} +``` + +**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 +// logUser(user: User): Result +// insertUser(user: User): ResultAsync +// 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) => {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 { + andThrough( + callback: (value: T) => Result + ): Result { ... } +} +``` + +**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 +// validateUser(user: User): Result +// insertUser(user: User): ResultAsync +// Note validateUser returns void upon success but insertUser takes User type. + +const resAsync = parseUserInput(userInput) + .andThrough(validateUser) + .asyncAndThen(insertUser) + +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, 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 +// insertUser(user: User): ResultAsync +// sendNotification(user: User): ResultAsync +// Note insertUser returns void upon success but sendNotification takes User type. + +const resAsync = parseUserInput(userInput) + .asyncAndThrough(insertUser) + .andThen(sendNotification) + +resAsync.then((res: Result) => {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. @@ -1096,7 +1231,101 @@ const resultMessage = await validateUser(user) [⬆️ Back to top](#toc) --- +#### `ResultAsync.andTee` (method) + +Takes a `ResultAsync` and lets the original `ResultAsync` 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 { + andTee( + callback: (value: T) => unknown + ): ResultAsync => { ... } +} +``` + +**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 +// logUser(user: User): Result +// sendNotification(user: User): ResultAsync +// 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) => {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 { + andThrough( + callback: (value: T) => Result | ResultAsync, + ): ResultAsync => { ... } +} +``` + +**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 +// insertUser(user: User): ResultAsync +// sendNotification(user: User): ResultAsync +// Note insertUser returns void upon success but sendNotification takes User type. + +const resAsync = buildUser(userRaw) + .andThrough(insertUser) + .andThen(sendNotification) + +resAsync.then((res: Result) => {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. From 456352e1099f8dbfd169abdae999785089b5d8f9 Mon Sep 17 00:00:00 2001 From: m-shaka Date: Thu, 29 Aug 2024 00:15:32 +0900 Subject: [PATCH 09/11] chore: add a missing changeset file --- .changeset/mighty-pans-promise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-pans-promise.md diff --git a/.changeset/mighty-pans-promise.md b/.changeset/mighty-pans-promise.md new file mode 100644 index 00000000..964f88f4 --- /dev/null +++ b/.changeset/mighty-pans-promise.md @@ -0,0 +1,5 @@ +--- +'neverthrow': minor +--- + +feat: add `andTee` and `andThrough` to handle side-effect From e4d4535d13fcdf44986791621c8e3de7ed9d6e95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Aug 2024 16:11:29 +0000 Subject: [PATCH 10/11] Version Packages --- .changeset/mighty-pans-promise.md | 5 ----- .changeset/thirty-grapes-drum.md | 5 ----- .changeset/three-mice-act.md | 5 ----- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 .changeset/mighty-pans-promise.md delete mode 100644 .changeset/thirty-grapes-drum.md delete mode 100644 .changeset/three-mice-act.md diff --git a/.changeset/mighty-pans-promise.md b/.changeset/mighty-pans-promise.md deleted file mode 100644 index 964f88f4..00000000 --- a/.changeset/mighty-pans-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'neverthrow': minor ---- - -feat: add `andTee` and `andThrough` to handle side-effect diff --git a/.changeset/thirty-grapes-drum.md b/.changeset/thirty-grapes-drum.md deleted file mode 100644 index 10867d68..00000000 --- a/.changeset/thirty-grapes-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neverthrow": patch ---- - -Fix `combineWithAllErrors` types diff --git a/.changeset/three-mice-act.md b/.changeset/three-mice-act.md deleted file mode 100644 index facb041a..00000000 --- a/.changeset/three-mice-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"neverthrow": patch ---- - -Made err() infer strings narrowly for easier error tagging. diff --git a/CHANGELOG.md b/CHANGELOG.md index cae573dc..e29f2539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # neverthrow +## 7.1.0 + +### Minor Changes + +- [#575](https://github.com/supermacro/neverthrow/pull/575) [`456352e`](https://github.com/supermacro/neverthrow/commit/456352e1099f8dbfd169abdae999785089b5d8f9) Thanks [@m-shaka](https://github.com/m-shaka)! - 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 diff --git a/package.json b/package.json index b0bd0b45..30a037a0 100644 --- a/package.json +++ b/package.json @@ -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", From 577e683bd6df1d8e17bdcbdc4672182ed21c6e3a Mon Sep 17 00:00:00 2001 From: m-shaka Date: Thu, 29 Aug 2024 01:16:48 +0900 Subject: [PATCH 11/11] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e29f2539..e1fbb517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Minor Changes -- [#575](https://github.com/supermacro/neverthrow/pull/575) [`456352e`](https://github.com/supermacro/neverthrow/commit/456352e1099f8dbfd169abdae999785089b5d8f9) Thanks [@m-shaka](https://github.com/m-shaka)! - feat: add `andTee` and `andThrough` to handle side-effect +- [#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