The problem
Say you must implement a web form to signup for an account. The form contains two field: username
and password
and the following validation rules must hold:
-
username
must not be empty -
username
can't have dashes in it -
password
needs to have at least 6 characters -
password
needs to have at least one capital letter -
password
needs to have at least one number
Either
The Either<E, A>
type represents a computation that might fail with an error of type E
or succeed with a value of type A
, so is a good candidate for implementing our validation rules.
For example let's encode each password
rule
import { Either, left, right } from 'fp-ts/Either'
const minLength = (s: string): Either<string, string> =>
s.length >= 6 ? right(s) : left('at least 6 characters')
const oneCapital = (s: string): Either<string, string> =>
/[A-Z]/g.test(s) ? right(s) : left('at least one capital letter')
const oneNumber = (s: string): Either<string, string> =>
/[0-9]/g.test(s) ? right(s) : left('at least one number')
We can chain all the rules using... chain
import { chain } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
const validatePassword = (s: string): Either<string, string> =>
pipe(
minLength(s),
chain(oneCapital),
chain(oneNumber)
)
Because we are using Either
the checks are fail-fast. That is, any failed check shortcircuits subsequent checks so we will only ever get one error.
console.log(validatePassword('ab'))
// => left("at least 6 characters")
console.log(validatePassword('abcdef'))
// => left("at least one capital letter")
console.log(validatePassword('Abcdef'))
// => left("at least one number")
However this could lead to a bad UX, it would be nice to have all of these errors be reported simultaneously.
The Validation
abstraction may help here.
Validation
Validations are much like Either<E, A>
, they represent a computation that might fail with an error of type E
or succeed with a value of type A
, but as opposed to the usual computations involving Either
, they are able to collect multiple failures.
In order to do that we must tell validations how to combine two values of type E
.
That's what Semigroup is all about: combining two values of the same type.
For example we can pack the errors into a non empty array.
The 'fp-ts/Either'
module provides a getValidation
function that, given a semigroup, returns an alternative Applicative instance for Either
import { getSemigroup } from 'fp-ts/NonEmptyArray'
import { getValidation } from 'fp-ts/Either'
const applicativeValidation = getValidation(getSemigroup<string>())
However in order to use applicativeValidation
we must first redefine all the rules so that they return a value of type Either<NonEmptyArray<string>, string>
.
Instead of rewriting all the previous functions, which is cumbersome, let's define a combinator that converts a check outputting an Either<E, A>
into a check outputting a Either<NonEmptyArray<E>, A>
import { NonEmptyArray } from 'fp-ts/NonEmptyArray'
import { mapLeft } from 'fp-ts/Either'
function lift<E, A>(check: (a: A) => Either<E, A>): (a: A) => Either<NonEmptyArray<E>, A> {
return a =>
pipe(
check(a),
mapLeft(a => [a])
)
}
const minLengthV = lift(minLength)
const oneCapitalV = lift(oneCapital)
const oneNumberV = lift(oneNumber)
Let's put all together, I'm going to use the sequenceT
helper which takes n
actions and does them from left-to-right, returning the resulting tuple
import { sequenceT } from 'fp-ts/Apply'
import { map } from 'fp-ts/Either'
function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
return pipe(
sequenceT(getValidation(getSemigroup<string>()))(
minLengthV(s),
oneCapitalV(s),
oneNumberV(s)
),
map(() => s)
)
}
console.log(validatePassword('ab'))
// => left(["at least 6 characters", "at least one capital letter", "at least one number"])
Appendix
Note that the sequenceT
helper is able to handle actions with different types:
interface Person {
name: string
age: number
}
// Person constructor
const toPerson = ([name, age]: [string, number]): Person => ({
name,
age
})
const validateName = (s: string): Either<NonEmptyArray<string>, string> =>
s.length === 0 ? left(['Invalid name']) : right(s)
const validateAge = (s: string): Either<NonEmptyArray<string>, number> =>
isNaN(+s) ? left(['Invalid age']) : right(+s)
function validatePerson(name: string, age: string): Either<NonEmptyArray<string>, Person> {
return pipe(
sequenceT(applicativeValidation)(validateName(name), validateAge(age)),
map(toPerson)
)
}
Top comments (15)
Hi Giulio, thanks for the great library and the great documentation. I'm following along, and I'd like to know how would you implement a function that takes a list of
Validators
and applies them to a single input, something along these lines:Notice that for simplicity I'm making the
Validation
type take aNonEmptyArray
of errors, since I'm mainly interested on how to combine the validations from a dynamically constructed array.Finally, if the list of validations is empty, we can assume the input is valid.
Thanks!
This signature is weird
If you already know that the input has type
A
, why are you validating in the first place?Check this out: Parse, don't validate
Ah, yes. That's a very good observation thanks. Most likely the
Validator
would be producing some "valid" version ofA
. Point taken.Now, if we come back to the original email address validation example, how would you combine a dynamically built list of email validation functions? Suppose we load the validations to the client from a service, that gives us the business rules we want to validate?
The current example takes a
NonEmptyArray
as imput, and I have not been able to find a way to combine validations from anArray
.Again, many thanks, and thanks for your suggestion.
Hi Leon, I'm a little late to the party, but here's a gist of an implementation that I think does what you want. For this example, we assume our application requires a string of type Name that satisfies 2 business rules: to be considered a valid Name, a candidate string must be at least 5 characters long, and it must contain the letter 't'. The types could be improved, and the function should pry be generalized, but... you get the idea at least.
Edit: For whatever reason I can't seem to embed a gist right now, so let's try a CodeSandbox.
Thanks Ross, this seems to be just what I was trying to achieve. Much appreciated.
Thank you so much for those articles, Fp-ts has done a lot to increase my speed everyday at work!
I'm struggling to figure what this would look like without the
sequenceT
helper. Would you have time to provide a snippet ?(I tried going through the sequenceT source but I can't quite figure out what's going on yet)
This is what
sequenceT
is doing under the hood (when specialized togetValidation(getSemigroup<string>())
+ three validations)Ohhhh, okay, the tuple of results is what you apply to. Thank you so much for your answer!
(And yes, going back to the applicative article right now 😉)
How would this look in 2.0.0-rc.7?
For starters
getArraySemigroup
andgetApplicative
seems to be missing :)With
fp-ts@2
will be something likeI've been following your blog series, and am keen to better understand how this applicative validation works so I pasted your sample above into my code. Unfortunately I get "Argument of type 'Either, [string, string, string]>' is not assignable to parameter of type 'unknown[]'." error on the sequenceT(getValidation...line above. Using fp-ts @ 2.2.0 and Typescript @ 3.7.3. What would be expecting a parameter of type unknown[]?
Sorry, I can't repro, the sample above looks fine
how to extract the left validation messages or right person object from the
validatePerson
methodYou can fold the result
Thats gr8 !
function lift(check: (a: A) => Either): (a: A) => Either, A> {