diff --git a/src/EitherAsync.test.ts b/src/EitherAsync.test.ts index 80d493b7..f55a867c 100644 --- a/src/EitherAsync.test.ts +++ b/src/EitherAsync.test.ts @@ -42,6 +42,14 @@ describe('EitherAsync', () => { expect(await ea.run()).toEqual(Left('should show')) }) + test('promise interface', async () => { + const newEitherAsync = EitherAsync(() => Promise.resolve(5)); + expect(await newEitherAsync).toEqual(Right(5)); + + const newEitherAsync2 = EitherAsync(() => Promise.reject('nope')); + expect(await newEitherAsync2).toEqual(Left('nope')); +}); + test('map', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map( (_) => 'val' @@ -54,6 +62,18 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right('val')) }) + test('map async', async () => { + const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map( + async (_) => 'val' + ) + const newEitherAsync2 = EitherAsync(() => Promise.resolve(5))[ + 'fantasy-land/map' + ](async (_) => 'val') + + expect(await newEitherAsync.run()).toEqual(Right('val')) + expect(await newEitherAsync2.run()).toEqual(Right('val')) + }); + test('mapLeft', async () => { const newEitherAsync = EitherAsync(() => Promise.reject(0) @@ -67,6 +87,19 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right(0)) }) + test('mapLeft async', async () => { + const newEitherAsync = EitherAsync(() => + Promise.reject(0) + ).mapLeft(async (x) => x + 1) + + const newEitherAsync2 = EitherAsync(() => + Promise.resolve(0) + ).mapLeft(async (x) => x + 1) + + expect(await newEitherAsync).toEqual(Left(1)) + expect(await newEitherAsync2).toEqual(Right(0)) + }) + test('chain', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)).chain((_) => EitherAsync(() => Promise.resolve('val')) @@ -79,6 +112,30 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right('val')) }) + test('chain Either', async () => { + const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + v => Right(v + 1) + ); + expect(await chainedEitherAsync).toEqual(Right(6)); + + const failedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + _ => Left('failed') + ); + expect(await failedEitherAsync).toEqual(Left('failed')); + }) + + test('chain Promsie', async () => { + const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + async (v) => Right(v + 1) + ); + expect(await chainedEitherAsync).toEqual(Right(6)); + + const failedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + async (_) => Left('failed') + ); + expect(await failedEitherAsync).toEqual(Left('failed')); + }) + test('chainLeft', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5) @@ -91,6 +148,30 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right(6)) }) + test('chainLeft Either', async () => { + const newEitherAsync = EitherAsync(() => + Promise.resolve(5) + ).chainLeft(_ => Right(0)); + const newEitherAsync2 = EitherAsync(() => + Promise.reject(5) + ).chainLeft(v => Right(v + 1)); + + expect(await newEitherAsync.run()).toEqual(Right(5)) + expect(await newEitherAsync2.run()).toEqual(Right(6)) + }) + + test('chainLeft Promise', async () => { + const newEitherAsync = EitherAsync(() => + Promise.resolve(5) + ).chainLeft(async _ => Right(0)); + const newEitherAsync2 = EitherAsync(() => + Promise.reject(5) + ).chainLeft(async v => Right(v + 1)); + + expect(await newEitherAsync.run()).toEqual(Right(5)) + expect(await newEitherAsync2.run()).toEqual(Right(6)) + }) + test('toMaybeAsync', async () => { const ma = EitherAsync(({ liftEither }) => liftEither(Left('123'))) diff --git a/src/EitherAsync.ts b/src/EitherAsync.ts index f4d46c41..06a245eb 100644 --- a/src/EitherAsync.ts +++ b/src/EitherAsync.ts @@ -1,7 +1,9 @@ import { Either, Left, Right } from './Either' import { MaybeAsync } from './MaybeAsync' -export interface EitherAsync { +type EitherAsyncLike = Either | Promise>; + +export interface EitherAsync extends Promise> { /** * It's important to remember how `run` will behave because in an * async context there are other ways for a function to fail other @@ -17,13 +19,13 @@ export interface EitherAsync { */ run(): Promise> /** Transforms the `Right` value of `this` with a given function. If the EitherAsync that is being mapped resolves to a Left then the mapping function won't be called and `run` will resolve the whole thing to that Left, just like the regular Either#map */ - map(f: (value: R) => R2): EitherAsync + map(f: (value: R) => R2 | PromiseLike): EitherAsync /** Maps the `Left` value of `this`, acts like an identity if `this` is `Right` */ - mapLeft(f: (value: L) => L2): EitherAsync + mapLeft(f: (value: L) => L2 | PromiseLike): EitherAsync /** Transforms `this` with a function that returns a `EitherAsync`. Behaviour is the same as the regular Either#chain */ - chain(f: (value: R) => EitherAsync): EitherAsync + chain(f: (value: R) => EitherAsyncLike): EitherAsync /** The same as EitherAsync#chain but executes the transformation function only if the value is Left. Useful for recovering from errors */ - chainLeft(f: (value: L) => EitherAsync): EitherAsync + chainLeft(f: (value: L) => EitherAsyncLike): EitherAsync /** Converts `this` to a MaybeAsync, discarding any error values */ toMaybeAsync(): MaybeAsync /** Returns `Right` if `this` is `Left` and vice versa */ @@ -75,33 +77,33 @@ class EitherAsyncImpl implements EitherAsync { } } - map(f: (value: R) => R2): EitherAsync { - return EitherAsync((helpers) => this.runPromise(helpers).then(f)) + map(f: (value: R) => R2 | PromiseLike): EitherAsync { + return EitherAsync((helpers) => this.runPromise(helpers).then(f)); } - mapLeft(f: (value: L) => L2): EitherAsync { + mapLeft(f: (value: L) => L2 | PromiseLike): EitherAsync { return EitherAsync(async (helpers) => { try { return await this.runPromise((helpers as any) as EitherAsyncHelpers) } catch (e) { - throw f(e) + throw await f(e); } }) } - chain(f: (value: R) => EitherAsync): EitherAsync { + chain(f: (value: R) => EitherAsyncLike): EitherAsync { return EitherAsync(async (helpers) => { const value = await this.runPromise(helpers) - return helpers.fromPromise(f(value).run()) + return helpers.fromPromise(Promise.resolve(f(value))) }) } - chainLeft(f: (value: L) => EitherAsync): EitherAsync { + chainLeft(f: (value: L) => EitherAsyncLike): EitherAsync { return EitherAsync(async (helpers) => { try { return await this.runPromise((helpers as any) as EitherAsyncHelpers) } catch (e) { - return helpers.fromPromise(f(e).run()) + return helpers.fromPromise(Promise.resolve(f(e))); } }) } @@ -130,6 +132,23 @@ class EitherAsyncImpl implements EitherAsync { ): EitherAsync { return this.chain(f) } + + async then, TResult2 = never>( + onfulfilled?: ((value: Either) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null + ): Promise { + return this.run().then(onfulfilled, onrejected); + } + + async catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise | TResult> { + return this.run().catch(onrejected); + } + + async finally(onfinally?: (() => void) | undefined | null): Promise> { + return this.run().finally(onfinally); + } + + readonly [Symbol.toStringTag]: string; } /** Constructs an EitherAsync object from a function that takes an object full of helpers that let you lift things into the EitherAsync context and returns a Promise */