Hi, I'm revisiting io-ts after being away for a while. I'm experimenting with it in an express app and I'm not sure of the best code style approach. Here's an example:
import {Router} from 'express'
import ash from 'express-async-handler'
import {isLeft} from 'fp-ts/lib/Either'
import * as t from 'io-ts'
const ID = t.brand(
t.number,
(i: any): i is t.Branded<number, {readonly ID: unique symbol}> =>
Number.isSafeInteger(i) && i >= 1,
'ID'
)
type ID = t.TypeOf<typeof ID>
const router = Router()
router.get('/:id', ash(async (req, res) => {
const idResult = ID.decode(Number(req.params.id))
if (isLeft(idResult)) {
return res.status(HttpStatus.BAD_REQUEST).json({
message: 'Invalid User ID'
})
}
const id = idResult.right
const userResult = await getUserByID(id)
return isRight(userResult)
? res.json(userResult.right)
: res.status(HttpStatus.NOT_FOUND).json({
message: 'User not found for this ID'
})
}))
I looked at using pipe and fold, but the code seems to get very verbose and very indented. I also needed to use async callbacks... I wasn't sure if errors would be caught properly by Express. Is there a better way to write this?
Also another question, why are the types not inferred here:
fold(l => {
// l is unknown
}, r => {
// r is unknown
})(ID.decode(input))
I think it used to work in V1, if you did T.decode(input).fold(l => {}, r => {})
Hi @spacejack , to answer your second question, with fp-ts v2 you'll have to use pipe together with fold to get the correct inference:
import { pipe } from 'fp-ts/lib/pipeable'
import { fold } from 'fp-ts/lib/Either'
pipe(
ID.decode(input),
fold(
l => { /* l is t.Errors */ },
r => { /* r is ID */ }
)
)
Regarding your first question, I would say it depends on your familiarity with other fp concepts, and the willingness to use them in the codebase. For instance, mixing Promises with fp-ts data types is not really advisable in my experience, i.e. you'll keep switching between two different APIs that can't be composed (async/await or Promises in general, versus fp-ts pipes)
Just to give a quick example, If you are willing to switch from Promise<Either> to TaskEither inside this codebase, your example above could be rewritten into something like:
import * as E from "fp-ts/lib/Either"
import * as T from "fp-ts/lib/Task"
import * as TE from "fp-ts/lib/TaskEither"
import { pipe } from "fp-ts/lib/pipeable"
// provided some helpers with different signatures...
type User = { name: string }
declare function getUserById(id: ID): TE.TaskEither<HttpStatus, User>
declare function writeErrorResponse(status: HttpStatus): T.Task<void>
declare function writeSuccessResponse(body: User): T.Task<void>
// the async function body could look something similar to:
return pipe(
ID.decode(req.params.id),
E.mapLeft(() => HttpStatus.BAD_REQUEST),
TE.fromEither,
TE.chain(getUserById),
TE.fold(writeErrorResponse, writeSuccessResponse)
)()
Just take this as a possible solution, I'm not saying you are forced to go in this direction to use io-ts.
Hope this helps :)
@giogonzo great answer! Thanks very much, it clears up a lot. So if I want to keep using async/await, it looks like isLeft/isRight make the imperative style fairly workable.
Oh, one other question - what happens to an (unexpected) exception thrown during the piped operations?
I suppose by "unexpected exception" you mean throw new Error() or Promise.reject()?
fp-ts expects to work with pure function implementations - always.
For this reason, throw new Error() as well as Promise.reject() is always unexpected and unhandled by the library itself.
If dealing with 3rd parties libraries / APIs where the above is not true, explicit fp-ts APIs are available to resume the computation assuming it's pure and the above assumption holds from there onwards. As described in "Interoperability with non-functional code":
Either.tryCatch/Option.tryCatch will catch thrown errors (as in try {} catch {})TaskEither.tryCatch will catch promise rejections
Most helpful comment
Hi @spacejack , to answer your second question, with fp-ts v2 you'll have to use
pipetogether withfoldto get the correct inference:Regarding your first question, I would say it depends on your familiarity with other fp concepts, and the willingness to use them in the codebase. For instance, mixing Promises with fp-ts data types is not really advisable in my experience, i.e. you'll keep switching between two different APIs that can't be composed (async/await or Promises in general, versus fp-ts pipes)
Just to give a quick example, If you are willing to switch from
Promise<Either>toTaskEitherinside this codebase, your example above could be rewritten into something like:Just take this as a possible solution, I'm not saying you are forced to go in this direction to use
io-ts.Hope this helps :)