typescriptfunctional-programmingfp-ts

fp-ts best way to work with TaskEither wrapping an Option


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:

  1. Found the item
  2. Couldn't find item with the ID
  3. Some error (ie DB connection)

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:

  1. 200 + payload
  2. 404
  3. 500

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))
    }

Solution

  • 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.