From a14cd6a1cd9d6fc2f37372702b6970a14e5ff97c Mon Sep 17 00:00:00 2001 From: marvin-j97 Date: Thu, 27 Mar 2025 16:50:07 +0100 Subject: [PATCH 1/3] feat: added inspect, inspectErr --- src/result-async.ts | 80 ++++++++++++++------- src/result.ts | 108 +++++++++++++++++++--------- tests/index.test.ts | 172 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 293 insertions(+), 67 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index 20120d2b..1a4d926f 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -98,6 +98,19 @@ export class ResultAsync implements PromiseLike> { ) } + inspect(f: (t: T) => void | 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) + }), + ) + } + andThrough(f: (t: T) => Result | ResultAsync): ResultAsync { return new ResultAsync( this._promise.then(async (res: Result) => { @@ -158,6 +171,19 @@ export class ResultAsync implements PromiseLike> { ) } + inspectErr(f: (e: E) => void | Promise): ResultAsync { + return new ResultAsync( + this._promise.then(async (res: Result) => { + if (res.isOk()) { + return new Ok(res.value) + } + + await f(res.error) + return new Err(res.error) + }), + ) + } + andThen>( f: (t: T) => R, ): ResultAsync, InferErrTypes | E> @@ -278,50 +304,50 @@ export type CombineResultsWithAllErrorsArrayAsync< // Unwraps the inner `Result` from a `ResultAsync` for all elements. type UnwrapAsync = IsLiteralArray extends 1 ? Writable extends [infer H, ...infer Rest] - ? H extends PromiseLike - ? HI extends Result - ? [Dedup, ...UnwrapAsync] - : never - : never - : [] + ? H extends PromiseLike + ? HI extends Result + ? [Dedup, ...UnwrapAsync] + : never + : never + : [] : // If we got something too general such as ResultAsync[] then we // simply need to map it to ResultAsync. Yet `ResultAsync` // itself is a union therefore it would be enough to cast it to Ok. T extends Array ? A extends PromiseLike - ? HI extends Result - ? Ok[] - : never - : never + ? HI extends Result + ? Ok[] + : never + : never : never // Traverse through the tuples of the async results and create one // `ResultAsync` where the collected tuples are merged. type TraverseAsync = IsLiteralArray extends 1 ? Combine extends [infer Oks, infer Errs] - ? ResultAsync, MembersToUnion> - : never + ? ResultAsync, MembersToUnion> + : never : // The following check is important if we somehow reach to the point of // checking something similar to ResultAsync[]. In this case we don't // know the length of the elements, therefore we need to traverse the X and Y // in a way that the result should contain X[] and Y[]. T extends Array ? // The MemberListOf here is to include all possible types. Therefore - // if we face (ResultAsync | ResultAsync)[] this type should - // handle the case. - Combine, Depth> extends [infer Oks, infer Errs] - ? // The following `extends unknown[]` checks are just to satisfy the TS. - // we already expect them to be an array. - Oks extends unknown[] - ? Errs extends unknown[] - ? ResultAsync, MembersToUnion> - : ResultAsync, Errs> - : // The rest of the conditions are to satisfy the TS and support - // the edge cases which are not really expected to happen. - Errs extends unknown[] - ? ResultAsync> - : ResultAsync - : never + // if we face (ResultAsync | ResultAsync)[] this type should + // handle the case. + Combine, Depth> extends [infer Oks, infer Errs] + ? // The following `extends unknown[]` checks are just to satisfy the TS. + // we already expect them to be an array. + Oks extends unknown[] + ? Errs extends unknown[] + ? ResultAsync, MembersToUnion> + : ResultAsync, Errs> + : // The rest of the conditions are to satisfy the TS and support + // the edge cases which are not really expected to happen. + Errs extends unknown[] + ? ResultAsync> + : ResultAsync + : never : never // This type is similar to the `TraverseAsync` while the errors are also diff --git a/src/result.ts b/src/result.ts index ad447caa..5835e45b 100644 --- a/src/result.ts +++ b/src/result.ts @@ -156,6 +156,15 @@ interface IResult { */ map(f: (t: T) => A): Result + /** + * Calls a function with a the contained error if `Ok` value, leaving both `Ok` and `Err` untouched. + * + * This function can be used to perform side-effects without transforming the contained value. + * + * @param f a callback function that receives the contained `Ok` value + */ + inspect(f: (t: T) => void): Result + /** * Maps a `Result` to `Result` by applying a function to a * contained `Err` value, leaving an `Ok` value untouched. @@ -167,6 +176,15 @@ interface IResult { */ mapErr(f: (e: E) => U): Result + /** + * Calls a function with a the contained error if `Err`, leaving both `Ok` and `Err` untouched. + * + * This function can be used to perform side-effects without transforming the contained error. + * + * @param f a callback function that receives the contained `Err` value + */ + inspectErr(f: (e: E) => void): Result + /** * Similar to `map` Except you must return a new `Result`. * @@ -310,7 +328,7 @@ interface IResult { } export class Ok implements IResult { - constructor(readonly value: T) {} + constructor(readonly value: T) { } isOk(): this is Ok { return true @@ -324,11 +342,21 @@ export class Ok implements IResult { return ok(f(this.value)) } + inspect(f: (t: T) => void): Result { + f(this.value) + return ok(this.value) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars mapErr(_f: (e: E) => U): Result { return ok(this.value) } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inspectErr(_f: (e: E) => void): Result { + return ok(this.value) + } + andThen>( f: (t: T) => R, ): Result, InferErrTypes | E> @@ -417,7 +445,7 @@ export class Ok implements IResult { } export class Err implements IResult { - constructor(readonly error: E) {} + constructor(readonly error: E) { } isOk(): this is Ok { return false @@ -436,6 +464,16 @@ export class Err implements IResult { return err(f(this.error)) } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inspect(_f: (t: T) => void): Result { + return err(this.error) + } + + inspectErr(f: (e: E) => void): Result { + f(this.error) + return err(this.error) + } + andThrough(_f: (t: T) => Result): Result { return err(this.error) } @@ -593,17 +631,17 @@ type CollectResults - ? // Continue collecting... - CollectResults< - // the rest of the elements - Rest, - // The collected - [...Collected, [L, R]], - // and one less of the current depth - Prev[Depth] - > - : never // Impossible + H extends Result + ? // Continue collecting... + CollectResults< + // the rest of the elements + Rest, + // The collected + [...Collected, [L, R]], + // and one less of the current depth + Prev[Depth] + > + : never // Impossible : Collected // Transposes an array @@ -617,14 +655,14 @@ export type Transpose< Depth extends number = 10 > = A extends [infer T, ...infer Rest] ? T extends [infer L, infer R] - ? Transposed extends [infer PL, infer PR] - ? PL extends unknown[] - ? PR extends unknown[] - ? Transpose - : never - : never - : Transpose - : Transposed + ? Transposed extends [infer PL, infer PR] + ? PL extends unknown[] + ? PR extends unknown[] + ? Transpose + : never + : never + : Transpose + : Transposed : Transposed // Combines the both sides of the array of the results into a tuple of the @@ -644,17 +682,17 @@ export type Combine = Transpose, // Deduplicates the result, as the result type is a union of Err and Ok types. export type Dedup = T extends Result ? [unknown] extends [RL] - ? Err - : Ok + ? Err + : Ok : T // Given a union, this gives the array of the union members. export type MemberListOf = ( (T extends unknown ? (t: T) => T : never) extends infer U - ? (U extends unknown ? (u: U) => unknown : never) extends (v: infer V) => unknown - ? V - : never - : never + ? (U extends unknown ? (u: U) => unknown : never) extends (v: infer V) => unknown + ? V + : never + : never ) extends (_: unknown) => infer W ? [...MemberListOf>, W] : [] @@ -670,10 +708,10 @@ export type EmptyArrayToNever = T exten ? never : NeverArrayToNever extends 1 ? T extends [never, ...infer Rest] - ? [EmptyArrayToNever] extends [never] - ? never - : T - : T + ? [EmptyArrayToNever] extends [never] + ? never + : T + : T : T // Converts the `unknown` items of an array to `never`s. @@ -687,10 +725,10 @@ export type MembersToUnion = T extends unknown[] ? T[number] : never // Checks if the given type is a literal array. export type IsLiteralArray = T extends { length: infer L } ? L extends number - ? number extends L - ? 0 - : 1 - : 0 + ? number extends L + ? 0 + : 1 + : 0 : 0 // Traverses an array of results and returns a single result containing diff --git a/tests/index.test.ts b/tests/index.test.ts index 9f089a9d..68c49fa4 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -58,6 +58,19 @@ describe('Result.Ok', () => { expect(mapFn).toHaveBeenCalledTimes(1) }) + it('Inspects an Ok value', () => { + const okVal = ok(12) + const inspectorFn = vitest.fn((number) => { + console.log("got OK value", number) + }) + + const inspected = okVal.inspect(inspectorFn) + + expect(inspected.isOk()).toBe(true) + expect(inspected._unsafeUnwrap()).toBe(12) + expect(inspectorFn).toHaveBeenCalledTimes(1) + }) + it('Skips `mapErr`', () => { const mapErrorFunc = vitest.fn((_error) => 'mapped error value') @@ -67,6 +80,15 @@ describe('Result.Ok', () => { expect(mapErrorFunc).not.toHaveBeenCalledTimes(1) }) + it('Skips `inspectErr`', () => { + const inspectFn = vitest.fn((_error) => 'mapped error value') + + const notMapped = ok(12).inspectErr(inspectFn) + + expect(notMapped.isOk()).toBe(true) + expect(inspectFn).not.toHaveBeenCalledTimes(1) + }) + describe('andThen', () => { it('Maps to an Ok', () => { const okVal = ok(12) @@ -137,7 +159,7 @@ describe('Result.Ok', () => { describe('andTee', () => { it('Calls the passed function but returns an original ok', () => { const okVal = ok(12) - const passedFn = vitest.fn((_number) => {}) + const passedFn = vitest.fn((_number) => { }) const teed = okVal.andTee(passedFn) @@ -162,7 +184,7 @@ 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 passedFn = vitest.fn((_number) => { }) const teed = errVal.orTee(passedFn) @@ -332,6 +354,20 @@ describe('Result.Err', () => { expect(hopefullyNotMapped._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr()) }) + it('Skips `inspect`', () => { + const errVal = err('I am your father') + + const inspectorFn = vitest.fn((_value) => { + console.log("got value") + }) + + const notInspected = errVal.inspect(inspectorFn) + + expect(notInspected.isErr()).toBe(true) + expect(inspectorFn).not.toHaveBeenCalled() + expect(notInspected._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr()) + }) + it('Maps over an Err', () => { const errVal = err('Round 1, Fight!') @@ -344,6 +380,20 @@ describe('Result.Err', () => { expect(mapped._unsafeUnwrapErr()).not.toEqual(errVal._unsafeUnwrapErr()) }) + it('Inspects an Err', () => { + const errVal = err('Round 1, Fight!') + + const inspectorFn = vitest.fn((error: string) => { + console.error("Error happened", error) + }) + + const inspected = errVal.inspectErr(inspectorFn) + + expect(inspected.isErr()).toBe(true) + expect(inspectorFn).toHaveBeenCalledTimes(1) + expect(inspected._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr()) + }) + it('unwrapOr and return the default value', () => { const okVal = err('Oh nooo') expect(okVal.unwrapOr(1)).toEqual(1) @@ -376,7 +426,7 @@ describe('Result.Err', () => { it('Skips over andTee', () => { const errVal = err('Yolo') - const mapper = vitest.fn((_val) => {}) + const mapper = vitest.fn((_val) => { }) const hopefullyNotFlattened = errVal.andTee(mapper) @@ -877,6 +927,118 @@ describe('ResultAsync', () => { }) }) + describe("inspect", () => { + it('Inspects a value using a synchronous function', async () => { + const asyncVal = okAsync(12) + + const inspectorFn = vitest.fn((number) => { + console.log("got value", number) + }) + + const inspected = asyncVal.inspect(inspectorFn) + + expect(inspected).toBeInstanceOf(ResultAsync) + + const newVal = await inspected + + expect(newVal.isOk()).toBe(true) + expect(newVal._unsafeUnwrap()).toBe(12) + expect(inspectorFn).toHaveBeenCalledTimes(1) + }) + + it('Inspects a value using a asynchronous function', async () => { + const asyncVal = okAsync(12) + + const inspectorFn = vitest.fn(async (number) => { + console.log("got value", number) + }) + + const inspected = asyncVal.inspect(inspectorFn) + + expect(inspected).toBeInstanceOf(ResultAsync) + + const newVal = await inspected + + expect(newVal.isOk()).toBe(true) + expect(newVal._unsafeUnwrap()).toBe(12) + expect(inspectorFn).toHaveBeenCalledTimes(1) + }) + + it('Skips an error', async () => { + const asyncErr = errAsync('Wrong format') + + const inspectorFn = vitest.fn((number) => { + console.log("got value", number) + }) + + const notInspected = asyncErr.inspect(inspectorFn) + + expect(notInspected).toBeInstanceOf(ResultAsync) + + const newVal = await notInspected + + expect(newVal.isErr()).toBe(true) + expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') + expect(inspectorFn).toHaveBeenCalledTimes(0) + }) + }) + + describe("inspectErr", () => { + it('Inspects an error using an synchronous function', async () => { + const asyncErr = errAsync('Wrong format') + + const inspectorFn = vitest.fn((str) => { + console.error("error happened", str) + }) + + const inspectedErr = asyncErr.inspectErr(inspectorFn) + + expect(inspectedErr).toBeInstanceOf(ResultAsync) + + const newVal = await inspectedErr + + expect(newVal.isErr()).toBe(true) + expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') + expect(inspectorFn).toHaveBeenCalledTimes(1) + }) + + it('Inspects an error using an asynchronous function', async () => { + const asyncErr = errAsync('Wrong format') + + const inspectorFn = vitest.fn(async (str) => { + console.error("error happened", str) + }) + + const inspectedErr = asyncErr.inspectErr(inspectorFn) + + expect(inspectedErr).toBeInstanceOf(ResultAsync) + + const newVal = await inspectedErr + + expect(newVal.isErr()).toBe(true) + expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') + expect(inspectorFn).toHaveBeenCalledTimes(1) + }) + + it('Skips a value', async () => { + const asyncVal = okAsync(12) + + const inspectorFn = vitest.fn((str) => { + console.error("got error", str) + }) + + const notInspected = asyncVal.inspectErr(inspectorFn) + + expect(notInspected).toBeInstanceOf(ResultAsync) + + const newVal = await notInspected + + expect(newVal.isOk()).toBe(true) + expect(newVal._unsafeUnwrap()).toBe(12) + expect(inspectorFn).toHaveBeenCalledTimes(0) + }) + }) + describe('mapErr', () => { it('Maps an error using a synchronous function', async () => { const asyncErr = errAsync('Wrong format') @@ -1067,7 +1229,7 @@ describe('ResultAsync', () => { describe('andTee', () => { it('Calls the passed function but returns an original ok', async () => { const okVal = okAsync(12) - const passedFn = vitest.fn((_number) => {}) + const passedFn = vitest.fn((_number) => { }) const teed = await okVal.andTee(passedFn) @@ -1092,7 +1254,7 @@ 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 passedFn = vitest.fn((_number) => { }) const teed = await errVal.orTee(passedFn) From 9e2cfa1f511c544c19e515c86c0b3904fd081349 Mon Sep 17 00:00:00 2001 From: marvin-j97 Date: Thu, 27 Mar 2025 16:51:33 +0100 Subject: [PATCH 2/3] format --- src/result-async.ts | 54 +++++++++++++++---------------- src/result.ts | 78 ++++++++++++++++++++++----------------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/result-async.ts b/src/result-async.ts index 1a4d926f..5e8f675d 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -304,50 +304,50 @@ export type CombineResultsWithAllErrorsArrayAsync< // Unwraps the inner `Result` from a `ResultAsync` for all elements. type UnwrapAsync = IsLiteralArray extends 1 ? Writable extends [infer H, ...infer Rest] - ? H extends PromiseLike - ? HI extends Result - ? [Dedup, ...UnwrapAsync] - : never - : never - : [] + ? H extends PromiseLike + ? HI extends Result + ? [Dedup, ...UnwrapAsync] + : never + : never + : [] : // If we got something too general such as ResultAsync[] then we // simply need to map it to ResultAsync. Yet `ResultAsync` // itself is a union therefore it would be enough to cast it to Ok. T extends Array ? A extends PromiseLike - ? HI extends Result - ? Ok[] - : never - : never + ? HI extends Result + ? Ok[] + : never + : never : never // Traverse through the tuples of the async results and create one // `ResultAsync` where the collected tuples are merged. type TraverseAsync = IsLiteralArray extends 1 ? Combine extends [infer Oks, infer Errs] - ? ResultAsync, MembersToUnion> - : never + ? ResultAsync, MembersToUnion> + : never : // The following check is important if we somehow reach to the point of // checking something similar to ResultAsync[]. In this case we don't // know the length of the elements, therefore we need to traverse the X and Y // in a way that the result should contain X[] and Y[]. T extends Array ? // The MemberListOf here is to include all possible types. Therefore - // if we face (ResultAsync | ResultAsync)[] this type should - // handle the case. - Combine, Depth> extends [infer Oks, infer Errs] - ? // The following `extends unknown[]` checks are just to satisfy the TS. - // we already expect them to be an array. - Oks extends unknown[] - ? Errs extends unknown[] - ? ResultAsync, MembersToUnion> - : ResultAsync, Errs> - : // The rest of the conditions are to satisfy the TS and support - // the edge cases which are not really expected to happen. - Errs extends unknown[] - ? ResultAsync> - : ResultAsync - : never + // if we face (ResultAsync | ResultAsync)[] this type should + // handle the case. + Combine, Depth> extends [infer Oks, infer Errs] + ? // The following `extends unknown[]` checks are just to satisfy the TS. + // we already expect them to be an array. + Oks extends unknown[] + ? Errs extends unknown[] + ? ResultAsync, MembersToUnion> + : ResultAsync, Errs> + : // The rest of the conditions are to satisfy the TS and support + // the edge cases which are not really expected to happen. + Errs extends unknown[] + ? ResultAsync> + : ResultAsync + : never : never // This type is similar to the `TraverseAsync` while the errors are also diff --git a/src/result.ts b/src/result.ts index 5835e45b..87667fdc 100644 --- a/src/result.ts +++ b/src/result.ts @@ -158,9 +158,9 @@ interface IResult { /** * Calls a function with a the contained error if `Ok` value, leaving both `Ok` and `Err` untouched. - * + * * This function can be used to perform side-effects without transforming the contained value. - * + * * @param f a callback function that receives the contained `Ok` value */ inspect(f: (t: T) => void): Result @@ -178,9 +178,9 @@ interface IResult { /** * Calls a function with a the contained error if `Err`, leaving both `Ok` and `Err` untouched. - * + * * This function can be used to perform side-effects without transforming the contained error. - * + * * @param f a callback function that receives the contained `Err` value */ inspectErr(f: (e: E) => void): Result @@ -328,7 +328,7 @@ interface IResult { } export class Ok implements IResult { - constructor(readonly value: T) { } + constructor(readonly value: T) {} isOk(): this is Ok { return true @@ -445,7 +445,7 @@ export class Ok implements IResult { } export class Err implements IResult { - constructor(readonly error: E) { } + constructor(readonly error: E) {} isOk(): this is Ok { return false @@ -631,17 +631,17 @@ type CollectResults - ? // Continue collecting... - CollectResults< - // the rest of the elements - Rest, - // The collected - [...Collected, [L, R]], - // and one less of the current depth - Prev[Depth] - > - : never // Impossible + H extends Result + ? // Continue collecting... + CollectResults< + // the rest of the elements + Rest, + // The collected + [...Collected, [L, R]], + // and one less of the current depth + Prev[Depth] + > + : never // Impossible : Collected // Transposes an array @@ -655,14 +655,14 @@ export type Transpose< Depth extends number = 10 > = A extends [infer T, ...infer Rest] ? T extends [infer L, infer R] - ? Transposed extends [infer PL, infer PR] - ? PL extends unknown[] - ? PR extends unknown[] - ? Transpose - : never - : never - : Transpose - : Transposed + ? Transposed extends [infer PL, infer PR] + ? PL extends unknown[] + ? PR extends unknown[] + ? Transpose + : never + : never + : Transpose + : Transposed : Transposed // Combines the both sides of the array of the results into a tuple of the @@ -682,17 +682,17 @@ export type Combine = Transpose, // Deduplicates the result, as the result type is a union of Err and Ok types. export type Dedup = T extends Result ? [unknown] extends [RL] - ? Err - : Ok + ? Err + : Ok : T // Given a union, this gives the array of the union members. export type MemberListOf = ( (T extends unknown ? (t: T) => T : never) extends infer U - ? (U extends unknown ? (u: U) => unknown : never) extends (v: infer V) => unknown - ? V - : never - : never + ? (U extends unknown ? (u: U) => unknown : never) extends (v: infer V) => unknown + ? V + : never + : never ) extends (_: unknown) => infer W ? [...MemberListOf>, W] : [] @@ -708,10 +708,10 @@ export type EmptyArrayToNever = T exten ? never : NeverArrayToNever extends 1 ? T extends [never, ...infer Rest] - ? [EmptyArrayToNever] extends [never] - ? never - : T - : T + ? [EmptyArrayToNever] extends [never] + ? never + : T + : T : T // Converts the `unknown` items of an array to `never`s. @@ -725,10 +725,10 @@ export type MembersToUnion = T extends unknown[] ? T[number] : never // Checks if the given type is a literal array. export type IsLiteralArray = T extends { length: infer L } ? L extends number - ? number extends L - ? 0 - : 1 - : 0 + ? number extends L + ? 0 + : 1 + : 0 : 0 // Traverses an array of results and returns a single result containing From 2a95ea71ce33a931c734b2006ed20c4923da98d8 Mon Sep 17 00:00:00 2001 From: Marvin <33938500+marvin-j97@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:47:02 +0100 Subject: [PATCH 3/3] Update result.ts --- src/result.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/result.ts b/src/result.ts index 87667fdc..6d57b3f2 100644 --- a/src/result.ts +++ b/src/result.ts @@ -157,7 +157,7 @@ interface IResult { map(f: (t: T) => A): Result /** - * Calls a function with a the contained error if `Ok` value, leaving both `Ok` and `Err` untouched. + * Calls a function with the contained `Ok` value, leaving both `Ok` and `Err` untouched. * * This function can be used to perform side-effects without transforming the contained value. * @@ -177,7 +177,7 @@ interface IResult { mapErr(f: (e: E) => U): Result /** - * Calls a function with a the contained error if `Err`, leaving both `Ok` and `Err` untouched. + * Calls a function with the contained `Err` error, leaving both `Ok` and `Err` untouched. * * This function can be used to perform side-effects without transforming the contained error. *