Flexible validation with fp-ts
Tags: functional, typescript, fpts
Published: 2020-02-29 00:12:14

The `Either` monad is a good way to represent the result of a computation, you either have the happy path (conventionally on the `Right`) or the error (on the `Left`).

I've been working on a problem where I had a third case: the function could succeed with non-fatal warnings.

One way to model this in `fp-ts` is `fp-ts/lib/These`, from the docs:
> `These` is a data structure providing "inclusive-or" as opposed to `Either`'s "exclusive-or". If you interpret `Either<E, A>` as suggesting the computation may either fail or succeed (exclusively), then `These<E, A>` may fail, succeed, or do both at the same time.

On the `Left` a `ReadOnlyNonEmptyArray` that will contain our `Warnings`, on the `Right` a password provided by the user. If both values are present it means that it passed the validation but there are some warnings the user should know about.

This example is meant to be a variation of a post ([Either vs Validation](https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja)) by [Giulio Canti](https://dev.to/gcanti), the author of [fp-ts](https://gcanti.github.io/fp-ts/).


I'm publishing this snippet because I haven't found many (if any) public code bases that make use of `These`, `pipeable`, `getMonad` and it could be useful to someone else (or myself in a few months).


```typescript
import * as E from 'fp-ts/lib/Either'
import * as TH from 'fp-ts/lib/These';
import {pipe, pipeable} from "fp-ts/lib/pipeable";
import * as RNEA from 'fp-ts/lib/ReadonlyNonEmptyArray';
import { Semigroup } from 'fp-ts/lib/Semigroup';

type Password = string;
type Warning = string
type Warnings = RNEA.ReadonlyNonEmptyArray<Warning>

const minLength = (s: string): E.Either<string, string> =>
  s.length >= 6 ? E.right(s) : E.left('at least 6 characters')

const atLeastOneCapital = (arg: Password): TH.These<Warnings, Password> => {
    if (!/[A-Z]/g.test(arg)) {
        return TH.both(["at least one capital"], arg + "Z")
    }
    return TH.right(arg);
};

const trimSpaces = (s: Password): TH.These<Warnings, Password> => {
    const res = s.trim();
    return (res === s) 
           ? TH.right(res) 
           : TH.both(["no trailing spaces allowed"], res)
}

const lift = (f: (s: string) => E.Either<string, string>): (s: string) => E.Either<Warnings, Password> => {
    return (a: Password): E.Either<Warnings, Password> => {
        return pipe(
            f(a),
            E.mapLeft(warning => [warning]));
    }
};

const {chain: theseChain} = pipeable(TH.getMonad(RNEA.getSemigroup() as Semigroup<Warnings>));

const validate = (s: Password) => {
    return pipe( 
        TH.right(s),
        theseChain(trimSpaces),
        theseChain(lift(minLength)),
        theseChain(atLeastOneCapital),
        TH.fold(
            warnings => `invalid password: ${warnings.join(", ")}`,
            pass => `password accepted`,
            (warnings, pass) => `this is your new password: "${pass}" (we had to improve it: ${warnings.join(", ")})`
        )
    )
}

console.log(validate("pass "));
// invalid password: no trailing spaces allowed, at least 6 characters

console.log(validate(" pass123!"));
// this is your new password: "pass123!Z" (we had to improve it: 
// no trailing spaces allowed, at least one capital)

console.log(validate("Correct Horse"));
// password accepted
```