Do you want to request a _feature_ or report a _bug_?
Feature request
What is the current behavior?
Lots of helpful methods on Either, but none that know about Promises.
What is the expected behavior?
If I've got a validated value from io-ts, I might want to pass the Right to an API, and hang on to error messages from the Left. I can do that with a sync API natively (type annotations are for clarity, normally they'd just be inferred):
const decodedNum: Validation<number> = t.number.decode(myObj);
const result: Validation<ApiResult> = decodedNum.map(syncApi.getResult);
but if the API is async, it'd be nice to be able to do this:
const decodedNum: Validation<number> = t.number.decode(myObj);
const result: Validation<ApiResult> = await decodedNum.mapAsync(asyncApi.getResultAsync);
i.e. this definition would be added to Left and Right:
mapAsync<B>(f: (a: A) => Promise<B>): Promise<Either<L, B>>;
As far as I can tell, the workaround for not having async support is doing something like this (there may be a way to avoid using as any, but the whole approach is quite ugly anyway):
const decodedNum: Validation<number> = t.number.decode(myObj);
const result = await new Promise<Validation<ApiResult>>(async resolve => {
if (decodedNum.isRight()) {
const apiResult = await asyncApi.getResultAsync(decodedNum.value);
resolve(decodedNum.map(() => apiResult));
} else {
resolve(decodedNum.map(() => null as any));
}
});
The same idea could apply to chain with chainAsync, and likely all of the other methods on Either that return another Either. map was just an example since it's the one I've used by far the most (chain being second most).
The idiomatic way to handle this use case is lifting the validation (Either monad) into another monadic context: if the API is async then it will return some kind of monadic context representing async computations, for example Task or TaskEither
import { TaskEither, fromEither } from 'fp-ts/lib/TaskEither'
import * as t from 'io-ts'
// lifts a validation using a specified error handler
const liftWith = <L>(handler: (errors: t.Errors) => L) => <A>(fa: t.Validation<A>): TaskEither<L, A> => {
return fromEither(fa.mapLeft(handler))
}
//
// Usage
//
declare function getResultAsync(n: number): TaskEither<Error, string>
const lift = liftWith(() => new Error('validation error'))
declare const decodedNum: t.Validation<number>
// result: TaskEither<Error, string>
const result = lift(decodedNum).chain(getResultAsync)
Ah, somehow missed the Task/TaskEither types. Thanks!
In 2020, this is
// lifts a validation using a specified error handler
const liftWith = <L>(handler: (errors: t.Errors) => L) => <A>(fa: t.Validation<A>): TE.TaskEither<L, A> => {
return TE.fromEither(mapLeft(handler)(fa))
}
Most helpful comment
Ah, somehow missed the
Task/TaskEithertypes. Thanks!