Reputation: 181
I'm learning FP with FP-TS and I just hit a road block:
I have the following function in my repository:
// this is the repository
export const findBook = (id: string) => TaskEither<Error, Option<ParsedBook>>
that part is easy and makes sense to me. The issue is when I try to call it from my controller:
// this is the controller
export const getBook = (req: unknown) => Task<string | ParsedBook>
Anyways, here's my getBook
and what I'm trying to do:
const getBook: (req: unknown) => T.Task<string | ParsedBook> = flow(
getBookByIdRequestV.decode, // returns Either<Errors, GetBookByIdRequest>
E.fold(
() => T.of('Bad request!'),
flow(
prop('params'),
prop('id'),
findBook, // returns TaskEither<Error, Option<ParsedBook>>
TE.fold(
() => T.of('Internal error.'),
O.fold(
() => T.of('Book not found.'),
(book) => T.of(book) // this line results in an error
)
)
)
)
)
The issue is that the above code gives me an error:
Type 'ParsedBook' is not assignable to type 'string'
I think the issue is that E.fold
expects the result of onLeft
and onRight
to return the same type:
fold<E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B): (ma: Either<E, A>) => B
However, it might not only return a Task<string>
, but a Task<ParsedBook>
as well.
I tried using foldW
, which widens the type, but same error as well.
I don't really know what to do; I feel like the way I am modelling the types in my code are bad?
Here's the Codesandbox if it helps: https://codesandbox.io/s/tender-chandrasekhar-5p2xm?file=/src/index.ts
Thank you!
Upvotes: 2
Views: 4079
Reputation: 14098
The error you get when trying to use foldW
is because Task<string> | Task<ParsedBook>
isn’t assignable to Task<string | ParsedBook>
.
Instead of unwrapping the TaskEither
using TE.fold
and then re-wrapping the values in a Task using T.of
, you could use T.map
with E.foldW
instead:
import { identity } from "fp-ts/function";
T.map(E.foldW(
() => "Internal error.",
O.foldW(() => "Book not found.", identity)
))
In fact, there’s an even simpler way to do this using O.getOrElseW
:
T.map(E.foldW(
() => "Internal error.",
O.getOrElseW(() => "Book not found.")
))
Full code:
const getBook: (req: unknown) => T.Task<string | ParsedBook> = flow(
getBookByIdRequestV.decode,
E.fold(
() => T.of("Bad request!"),
flow(
prop("params"),
prop("id"),
findBook,
T.map(E.foldW(
() => "Internal error.",
O.getOrElseW(() => "Book not found.")
))
)
)
)
Upvotes: 3
Reputation: 1181
Nice, I'm trying to learn more about FP myself.
I'm not sure if I understood what getBook
should be doing. But I would keep it's return as a TaskEither
because it should represent the possibility of failing.
When working with Functors/Monads I think of them like a box. And I want to keep my values save inside the box. If you look up fold
you will find it listed under destructors, that is because it kind of destroys your box. So avoid fold
, maybe do it at the end of your computation if you have to.
But how do we do anything with our values if they are stuck inside a box? map
is our first solution here because we can take a function that acts on values to now act on boxes. But it keeps the box around our values.
We can also swap out the Box with TE.fromEither
or TE.fromOption
, which keeps our value save inside its box.
And lastly we can chain
to keep only one box. Every time a map
would give us a box in a box, we can change it to chain
. But the type of box has to align, or you will have to transform it before.
With those in mind I would write getBook
like this:
const getBook2: (req: unknown) => TE.TaskEither<string, ParsedBook> = flow(
getBookByIdRequestV.decode,
E.mapLeft(() => 'Bad request!'),
E.map(({ params }) => params.id),
TE.fromEither,
TE.chain(flow(
findBook,
TE.mapLeft(() => 'internal error.')
)),
TE.chain(TE.fromOption(() => 'Book not found.'))
)
Hope this helps and I'm still learning myself. So let me know if there is a better way.
Upvotes: 1