Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a "tap"-method to perform side effects #456

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
) as CombineResultsWithAllErrorsArrayAsync<T>
}

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

await f(res.value)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the promise fails? Shouldn't this be wrapped in a try / catch? ResultAsync.fromPromise could also be used here, which requires a handler function in the event the promise fails.

Copy link
Author

@lauhon lauhon Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question @supermacro , I'm honestly not sure. Maybe that's a good indicator that I should add a test that covers that(?)

I pretty much copied the map implementation. The only difference is that instead of returning the result of the passed function, I execute the function and then return the previous value.

If I add a try-catch to the tap function, shouldn't I also add one to the map and mapErr functions? 🤔

return new Ok<T, E>(res.value)
}),
)
}

map<A>(f: (t: T) => A | Promise<A>): ResultAsync<A, E> {
return new ResultAsync(
this._promise.then(async (res: Result<T, E>) => {
Expand Down
17 changes: 17 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ interface IResult<T, E> {
*/
isErr(): this is Err<T, E>

/**
* Performs a side effect for the `Ok` variant of `Result`.
*
* @param f The function to apply an `OK` value
* @returns the result of applying `f` or an `Err` untouched
*/
tap(f: (t: T) => void): Result<T, E>

/**
* Maps a `Result<T, E>` to `Result<U, E>`
* by applying a function to a contained `Ok` value, leaving an `Err` value
Expand Down Expand Up @@ -201,6 +209,11 @@ export class Ok<T, E> implements IResult<T, E> {
return !this.isOk()
}

tap(f: (t: T) => void): Result<T, E> {
f(this.value)
return ok(this.value)
}

map<A>(f: (t: T) => A): Result<A, E> {
return ok(f(this.value))
}
Expand Down Expand Up @@ -264,6 +277,10 @@ export class Err<T, E> implements IResult<T, E> {
return !this.isOk()
}

tap(_f: (t: T) => void): Result<T, E> {
return err(this.error)
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
map<A>(_f: (t: T) => A): Result<A, E> {
return err(this.error)
Expand Down
162 changes: 107 additions & 55 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ describe('Result.Ok', () => {
expect(ok(42)).not.toEqual(ok(43))
})

it('Taps into an Ok value', () => {
const okVal = ok(12)

// value can be accessed, but is not changed
const sideEffect = jest.fn((number) => console.log(number))

const mapped = okVal.tap(sideEffect)

expect(mapped.isOk()).toBe(true)
expect(mapped._unsafeUnwrap()).toBe(12)
expect(sideEffect).toHaveBeenCalledTimes(1)
})

it('Maps over an Ok value', () => {
const okVal = ok(12)
const mapFn = jest.fn((number) => number.toString())
Expand Down Expand Up @@ -201,6 +214,18 @@ describe('Result.Err', () => {
expect(err(42)).not.toEqual(err(43))
})

it('Skips `tap`', () => {
const errVal = err('I am your father')

const sideEffect = jest.fn((_value) => console.log('noooo'))

const hopefullyNotMapped = errVal.tap(sideEffect)

expect(hopefullyNotMapped.isErr()).toBe(true)
expect(sideEffect).not.toHaveBeenCalled()
expect(hopefullyNotMapped._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr())
})

it('Skips `map`', () => {
const errVal = err('I am your father')

Expand Down Expand Up @@ -332,15 +357,15 @@ describe('Result.fromThrowable', () => {

// Added for issue #300 -- the test here is not so much that expectations are met as that the test compiles.
it('Accepts an inner function which takes arguments', () => {
const hello = (fname: string): string => `hello, ${fname}`;
const safeHello = Result.fromThrowable(hello);
const hello = (fname: string): string => `hello, ${fname}`
const safeHello = Result.fromThrowable(hello)

const result = hello('Dikembe');
const safeResult = safeHello('Dikembe');
const result = hello('Dikembe')
const safeResult = safeHello('Dikembe')

expect(safeResult).toBeInstanceOf(Ok);
expect(result).toEqual(safeResult._unsafeUnwrap());
});
expect(safeResult).toBeInstanceOf(Ok)
expect(result).toEqual(safeResult._unsafeUnwrap())
})

it('Creates a function that returns an err when the inner function throws', () => {
const thrower = (): string => {
Expand Down Expand Up @@ -375,7 +400,7 @@ describe('Result.fromThrowable', () => {
})

it('has a top level export', () => {
expect(fromThrowable).toBe(Result.fromThrowable)
expect(fromThrowable).toBe(Result.fromThrowable)
})
})

Expand Down Expand Up @@ -406,37 +431,34 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', () => {
type HeterogenousList = [ Result<string, string>, Result<number, number>, Result<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
ok('Yooooo'),
ok(123),
ok(true),
type HeterogenousList = [
Result<string, string>,
Result<number, number>,
Result<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], string | number | boolean>
const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)]

type ExpecteResult = Result<[string, number, boolean], string | number | boolean>

const result: ExpecteResult = Result.combine(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true])
})

it('Does not destructure / concatenate arrays', () => {
type HomogenousList = [
Result<string[], boolean>,
Result<number[], string>,
]
type HomogenousList = [Result<string[], boolean>, Result<number[], string>]

const homogenousList: HomogenousList = [
ok(['hello', 'world']),
ok([1, 2, 3])
]
const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])]

type ExpectedResult = Result<[ string[], number[] ], boolean | string>
type ExpectedResult = Result<[string[], number[]], boolean | string>

const result: ExpectedResult = Result.combine(homogenousList)

expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual([
['hello', 'world'],
[1, 2, 3],
])
})
})

Expand All @@ -445,7 +467,7 @@ describe('Utils', () => {
const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)]

const resultAsync: ResultAsync<number[], never[]> = ResultAsync.combine(asyncResultList)

expect(resultAsync).toBeInstanceOf(ResultAsync)

const result = await ResultAsync.combine(asyncResultList)
Expand Down Expand Up @@ -480,14 +502,14 @@ describe('Utils', () => {
okAsync('Yooooo'),
okAsync(123),
okAsync(true),
okAsync([ 1, 2, 3]),
okAsync([1, 2, 3]),
]

type ExpecteResult = Result<[ string, number, boolean, number[] ], string | number | boolean>
type ExpecteResult = Result<[string, number, boolean, number[]], string | number | boolean>

const result: ExpecteResult = await ResultAsync.combine(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [1, 2, 3]])
})
})
})
Expand Down Expand Up @@ -517,37 +539,34 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', () => {
type HeterogenousList = [ Result<string, string>, Result<number, number>, Result<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
ok('Yooooo'),
ok(123),
ok(true),
type HeterogenousList = [
Result<string, string>,
Result<number, number>,
Result<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], (string | number | boolean)[]>
const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)]

type ExpecteResult = Result<[string, number, boolean], (string | number | boolean)[]>

const result: ExpecteResult = Result.combineWithAllErrors(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true])
})

it('Does not destructure / concatenate arrays', () => {
type HomogenousList = [
Result<string[], boolean>,
Result<number[], string>,
]
type HomogenousList = [Result<string[], boolean>, Result<number[], string>]

const homogenousList: HomogenousList = [
ok(['hello', 'world']),
ok([1, 2, 3])
]
const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])]

type ExpectedResult = Result<[ string[], number[] ], (boolean | string)[]>
type ExpectedResult = Result<[string[], number[]], (boolean | string)[]>

const result: ExpectedResult = Result.combineWithAllErrors(homogenousList)

expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual([
['hello', 'world'],
[1, 2, 3],
])
})
})
describe('`ResultAsync.combineWithAllErrors`', () => {
Expand Down Expand Up @@ -575,15 +594,15 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', async () => {
type HeterogenousList = [ ResultAsync<string, string>, ResultAsync<number, number>, ResultAsync<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
okAsync('Yooooo'),
okAsync(123),
okAsync(true),
type HeterogenousList = [
ResultAsync<string, string>,
ResultAsync<number, number>,
ResultAsync<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], [string, number, boolean]>
const heterogenousList: HeterogenousList = [okAsync('Yooooo'), okAsync(123), okAsync(true)]

type ExpecteResult = Result<[string, number, boolean], [string, number, boolean]>

const result: ExpecteResult = await ResultAsync.combineWithAllErrors(heterogenousList)

Expand Down Expand Up @@ -676,6 +695,40 @@ describe('ResultAsync', () => {
})
})

describe('tap', () => {
it('Taps into an async value', async () => {
const asyncVal = okAsync(12)

const sideEffect = jest.fn((number) => console.log(number))

const mapped = asyncVal.tap(sideEffect)

expect(mapped).toBeInstanceOf(ResultAsync)

const newVal = await mapped

expect(newVal.isOk()).toBe(true)
expect(newVal._unsafeUnwrap()).toBe(12)
expect(sideEffect).toHaveBeenCalledTimes(1)
})

it('Skips an error when tapping into an asynchronous value', async () => {
const asyncErr = errAsync<number, string>('Wrong format')

const sideEffect = jest.fn((number) => console.log(number))

const notMapped = asyncErr.tap(sideEffect)

expect(notMapped).toBeInstanceOf(ResultAsync)

const newVal = await notMapped

expect(newVal.isErr()).toBe(true)
expect(newVal._unsafeUnwrapErr()).toBe('Wrong format')
expect(sideEffect).toHaveBeenCalledTimes(0)
})
})

describe('map', () => {
it('Maps a value using a synchronous function', async () => {
const asyncVal = okAsync(12)
Expand Down Expand Up @@ -709,7 +762,7 @@ describe('ResultAsync', () => {
expect(mapAsyncFn).toHaveBeenCalledTimes(1)
})

it('Skips an error', async () => {
it('Skips an error when mapping an asynchronous value', async () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const asyncErr = errAsync<number, string>('Wrong format')

const mapSyncFn = jest.fn((number) => number.toString())
Expand Down Expand Up @@ -831,7 +884,6 @@ describe('ResultAsync', () => {
const okVal = okAsync(12)
const errorCallback = jest.fn((_errVal) => errAsync<number, string>('It is now a string'))


const result = await okVal.orElse(errorCallback)

expect(result).toEqual(ok(12))
Expand Down