I have seen many examples of using TaskEither
to say make an hhtp request or read a file etc.
What I am trying to do is simulate finding item in a DB by ID, so possible outputs of the operation could be:
An interface for something like that would be TaskEither<Error, Option<A>>
:
type TaskEO<A> = TaskEither<Error, Option<A>>
Since I will be sending a result as an HTTP response (response to a GET
query), I want to be able to clearly distinguish between the 3 scenarios above. the response codes for each would be:
I have written the below code to process the 3 possible scenarios into appropriate HTTP response:
import * as O from "fp-ts/Option";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/function";
type TaskEO<A> = TE.TaskEither<Error, O.Option<A>>;
const getGoodStuff = (id: string): TaskEO<string> => TE.of(O.some(`result for ${id}`));
const getBadStuff = (id: string): TaskEO<string> =>
TE.left(new Error(`failed fetching ${id}`));
const getEmptyStuff = (id: string): TaskEO<string> => TE.of(O.none);
getGoodStuff("123")()
.then((e) =>
pipe(
e,
E.fold(
(error) => `500: Internal Server Error`,
(stuff) =>
pipe(
stuff,
O.match(
() => `404: Not Found Error`,
(value) => `200: Yay we got: "${value}"`
)
)
)
)
)
.then(console.log);
You can replace the getGoodStuff
call with any of the other get...Stuff
functions to see that it does indeed handle the different responses correctly! Great stuff!
However, and here is the question for YOU dear reader, I have a feeling that there is a smarter way of structuring this composition. Unfortunately my knowledge of fp-ts is still somewhat limited.
How would YOU write the code above?
Thanks x
EDIT I narrowed it down to something like this:
enum HttpResponseCode {
OK = 200,
NOT_FOUND = 404,
INTERNAL_SERVER_ERROR = 500
}
type HttpResponse = {
code: HttpResponseCode;
payload: unknown;
}
const toHttpResponse = <A>(e: E.Either<Error, O.Option<A>>): HttpResponse =>
E.fold(
(error) => ({ code: HttpResponseCode.INTERNAL_SERVER_ERROR, payload: "Internal Server Error" }),
O.match(
() => ({ code: HttpResponseCode.NOT_FOUND, payload: "Resource not found" }),
(value) => ({ code: HttpResponseCode.OK, payload: value })
)
)(e)
That can then be used with Express route handler along this way:
async (req, res) => {
await findStuffById(req.params.stuffId)()
.then(toHttpResponse)
.then(({ code, payload }) => res.status(code).send(payload))
}
What you ended up with in your edit is about as clean as it's going to get I think. You want to do something in all of the possible cases, so fold
-ing or match
-ing are the tools for the job.
If you find that you need to match this exact shape often and the body of the functions you write is constantly the same, you could consider writing a helper like:
function matchTaskEO<A, R>({
onError,
onNone,
onSome,
}: {
// Up to you if you want to name them or use positional arguments
// FP-TS opts for the latter but I find this easier to parse personally.
onError: (e: Error) => R,
onNone: () => R,
onSome: (a: A) => R,
}) {
return (taskEO: TaskEO<A>) => E.match(
onError,
O.match(onNone, onSome),
);
}
Which you could then use to implement toHttpResponse
:
const toHttpResponse = <A>(taskEO: TaskEO<A>) => matchTaskEO<A, HttpResponse>({
onError: (e) => ({
code: HttpResponseCode.INTERNAL_SERVER_ERROR,
payload: "Internal Server Error",
}),
onNone: () => ({
code: HttpResponseCode.NOT_FOUND,
payload: "Resource not found",
}),
onSome: (value) => ({ code: HttpResponseCode.OK, payload: value })
})(taskEO);
This flattens the definition out, although writing the explicit Either
and Option
matches doesn't look too unclear in this case anyway.