DEV Community

loading...

Getting started with fp-ts: Either vs Validation

Giulio Canti
Updated on ・3 min read

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/lib/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')
Enter fullscreen mode Exit fullscreen mode

We can chain all the rules using... chain

import { chain } from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/pipeable'

const validatePassword = (s: string): Either<string, string> =>
  pipe(
    minLength(s),
    chain(oneCapital),
    chain(oneNumber)
  )
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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/lib/Either' module provides a getValidation function that, given a semigroup, returns an alternative Applicative instance for Either

import { getSemigroup } from 'fp-ts/lib/NonEmptyArray'
import { getValidation } from 'fp-ts/lib/Either'

const applicativeValidation = getValidation(getSemigroup<string>())
Enter fullscreen mode Exit fullscreen mode

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/lib/NonEmptyArray'
import { mapLeft } from 'fp-ts/lib/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)
Enter fullscreen mode Exit fullscreen mode

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/lib/Apply'
import { map } from 'fp-ts/lib/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"])
Enter fullscreen mode Exit fullscreen mode

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)
  )
}
Enter fullscreen mode Exit fullscreen mode

Discussion (15)

Collapse
grancalavera profile image
Leon Coto • Edited

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:

import { Either } from "fp-ts/lib/Either";
import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray";

type Validation<E, A> = Either<NonEmptyArray<E>, A>;
type Validator<E, A> = (a: A) => Validation<E, A>;

declare function validations<E, A>(vs: Validator<E, A>[], a: A): Validation<E, A>;
Enter fullscreen mode Exit fullscreen mode

Notice that for simplicity I'm making the Validation type take a NonEmptyArray 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!

Collapse
gcanti profile image
Giulio Canti Author

This signature is weird

type Validator<E, A> = (a: A) => Validation<E, A>;

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

Collapse
grancalavera profile image
Leon Coto

Ah, yes. That's a very good observation thanks. Most likely the Validator would be producing some "valid" version of A. 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 an Array.

Again, many thanks, and thanks for your suggestion.

Thread Thread
rossh87 profile image
Ross Hunter • Edited

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.

Thread Thread
grancalavera profile image
Leon Coto

Thanks Ross, this seems to be just what I was trying to achieve. Much appreciated.

Collapse
isthatcentered profile image
Edouard Penin

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)

Collapse
gcanti profile image
Giulio Canti Author

This is what sequenceT is doing under the hood (when specialized to getValidation(getSemigroup<string>()) + three validations)

function specializedSequenceT(
  firstValidation: Either<NonEmptyArray<string>, string>,
  secondValidation: Either<NonEmptyArray<string>, string>,
  thirdValidation: Either<NonEmptyArray<string>, string>
): Either<NonEmptyArray<string>, [string, string, string]> {
  // Applicative instance for `Either<NonEmptyArray<string>, A>`
  const V = getValidation(getSemigroup<string>())

  // builds a tuple from three strings
  const tuple = (a: string) => (b: string) => (c: string): [string, string, string] => [a, b, c]

  // manual lifting, check out the "Lifting" section in "Getting started with fp-ts: Applicative"
  return V.ap(V.ap(V.map(firstValidation, tuple), secondValidation), thirdValidation)
}

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    specializedSequenceT(minLengthV(s), oneCapitalV(s), oneNumberV(s)),
    map(() => s)
  )
}
Collapse
isthatcentered profile image
Edouard Penin

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

Collapse
patroza profile image
Patrick Roza

How would this look in 2.0.0-rc.7?
For starters getArraySemigroup and getApplicative seems to be missing :)

Collapse
gcanti profile image
Giulio Canti Author

With fp-ts@2 will be something like

import { sequenceT } from 'fp-ts/lib/Apply'
import { Either, getValidation, left, map, mapLeft, right } from 'fp-ts/lib/Either'
import { getSemigroup, NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
import { pipe } from 'fp-ts/lib/pipeable'

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

function lift<L, A>(
  check: (a: A) => Either<L, A>
): (a: A) => Either<NonEmptyArray<L>, A> {
  return a =>
    pipe(
      check(a),
      mapLeft(a => [a])
    )
}

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    sequenceT(getValidation(getSemigroup<string>()))(
      lift(minLength)(s),
      lift(oneCapital)(s),
      lift(oneNumber)(s)
    ),
    map(() => s)
  )
}

console.log(validatePassword('ab'))
/*
=> left([ 'at least 6 characters',
    'at least one capital letter',
    'at least one number' ])
*/
Collapse
josephr5000 profile image
josephr5000

I'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[]?

Thread Thread
gcanti profile image
Giulio Canti Author

Sorry, I can't repro, the sample above looks fine

Collapse
pradeepdas profile image
pradeepdas

how to extract the left validation messages or right person object from the validatePerson method

Collapse
gcanti profile image
Giulio Canti Author

You can fold the result

Collapse
ashiltendonu profile image
Aşil Tendonu

Thats gr8 !
function lift(check: (a: A) => Either): (a: A) => Either, A> {