npm package: @nobsteam/flowval
Description: Composable and terse validation using the "parse vs validate" principle and a monadic flow
Author: @bs85
Latest version: 0.1.1

# flowval

Composable and terse validation using the "parse vs validate" principle and a monadic flow.

_I recommend reading the [Motivation](#motivation) section below to understand the problem this library aims to solve before you get into usage / api._

## Goals

1. Static type safety and narrowing
2. Runtime safety
3. No unnecessary cognitive load (it compiles, it works)
6. Ability to split and combine validation as needed
2. No repetitive code
3. Low boilerplate

## Usage

```TypeScript
import { struct, ok, err, isOk } from 'flowval'

type UserInput = {
  firstname: string | null
  email: string | null
}

type UserRequestInput = {
  message: string
  termsAccepted: boolean
}

const validateUser = (user: UserInput) => struct({
  // type is `string | null` here
  firstname: user.firstname
    ? ok(
      // Type is narrowed to `string` here
      user.firstname,
      // Warnings can be added to `ok()` or `err()`
      user.firstname.length < 2 ? ['suspiciously short name'] : []
    )
    // `err()` always takes an array of something to enable merging
    : err([new Error('firstname is required')]),
  email: user.email
    ? user.email.includes('@')
      ? ok(user.email)
      : err([new Error('invalid email')])
    : err([new Error('email is required')]),
})

const validateUserRequest = (request: UserRequestInput) => struct({
  // `message` is always valid
  message: ok(request.message),
  // A strict API might require this attribute to be `true` instead of `boolean`
  termsAccepted: request.termsAccepted
    ? ok(true)
    : err([new Error('terms must be accepted')]),
})

const processRequest = (userInput: UserInput, userRequestInput: UserRequestInput) => {
  const result = struct({
    user: validateUser(userInput),
    userRequest: validateUserRequest(userRequestInput),
  })

  if (isErr(result)) {
    // `result` is known to be an error and `result.details` is of type Error[]

    // Early return enables narrowing for the code following the `if` statement
    return result
  }

  // From here on, `result` is known to be valid and `result.data` is narrowed correctly.
  //   - `result.data.user` has type `{ firstname: string; email: string }`
  //   - `result.data.userRequest` has type `{ message: string; termsAccepted: true }`
  //   - `result.warnings` is populated with any warnings generated

  console.log(`Thank you for your request ${result.data.user.firstname}`)
}
```

## API

### `ok<T, W>(data: T, warnings: W[]): Ok<T, W>`

Create an `ok` result wrapping data of type `T` and optional warnings of type `W`.

### `err<E, W>(details: E, warnings: W[]): Err<E, W>`

Create an `err` result wrapping errors of type `E`. `E` can be of any type but needs to be an array to use `struct()` or `all()`

### `isOk<T, E, W>(result: Result<T, E, W>): result is Ok<T, W>`

Check if `result` is `ok` and narrows the type from `Result<T, E, W>` to `Ok<T, W>`.

### `isErr<T, E, W>(result: Result<T, E, W>): result is Err<E, W>`

Check if `result` is `err` and narrows the type from `Result<T, E, W>` to `Err<E, W>`.

### `struct(results: Record<string, Result<unknown, unknown[], unknown>>)`

Takes an object containing one or more results. If all results are `ok`, returns a new object by unwrapping the data from every attribute. If at least one result is `err`, returns an `err` aggregating all the errors.

Each result can have different types for the wrapped data as well as errors and warnings. The types are mapped and merged correctly in the returned object.

```TypeScript
type Input = {
  firstname: string | null
  email: string | null
}

const validate = (input: Input) => struct({
  firstname: input.firstname ? ok(input.firstname) : err(['firstname is required']),
  email: input.email
    ? email.includes('@')
      ? ok(input.email)
      : err(['invalid email'])
    : err(['email is required']),
})

const validInput: Input = { firstname: 'John', email: 'john@example.com' }
// `validInput.firstname` has type string | null
const valid = validate(validInput)

if (isOk(valid)) {
  // `result.data.firstname` has type string
}

const invalidInput: Input = { firstname: null, email: 'johnny' }
const invalid = validate(invalidInput)

if (isErr(valid)) {
  // `result.details` is `['Firstname is required', 'Invalid email']`
}
```

### `all(...results: Result<unknown, unknown[], unknown>[])`

Similar to `struct()` but results are passed as an array instead of being named attributes in an object. Name inspired by `Promise.all`.

Takes an array of results. If all results are `ok`, returns a new result containing an array of their unwrapped data and aggregated warnings. If any result is `err`, returns an array of all the errors.

```TypeScript
const result1 = ok(1, ['warning1'])
const result2 = ok('yes', ['warning2'])
const result3 = err(['error3'])

const okResult = all(result1, result2)

if (isOk(okResult)) {
  console.log(okResult.data) // [1, 'yes']
  console.log(okResult.warnings) // ['warning1', 'warning2']
}

const errResults = all(result1, result2, result3);

if (isErr(errResults)) {
  console.log(errResults.details) // ['error3']
}
```

## Motivation

The pattern provided by this library is the result of a lot of experimentation to find the best way to validate data within complex processes involving a lot of input and multiple steps.

The rise of TypeScript has drastically improved how data is handled, thanks to language features like type narrowing and type guards. It has also invalidated many patterns that were previously used to validate data in untyped JavaScript:

```js
if (validate(inputData)) {
  // won't work if `apiCall` is strict
  return apiCall(inputData)
}
```

To remediate that, various patterns have been adopted, one of them being [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). In summary, the validation function should either return valid data or an error.

```TypeScript
if (user.firstname && user.email) {
  // firstname and email are narrowed to `string`
  return ok({ firstname: user.firstname, email: user.email })
}
```

Error generation can be done by throwing errors:

```TypeScript
if (!user.firstname) {
  throw new Error('firstname is required')
}

if (!user.email) {
  throw new Error('email is required')
}

if (!validEmail(user.email)) {
  throw new Error('email is invalid')
}

// no need for the `if`, narrowing works with exceptions
return ok({ firstname: user.firstname, email: user.email })
```

Bailing on the first error is often not what we want. Instead, we should accumulate errors and return them all at once:

```TypeScript
if (!user.firstname) {
  errors.push('firstname is required')
}

if (!user.email) {
  errors.push('email is required')
}

if (!validEmail(user.email)) {
  errors.push('email is invalid')
}

if (user.firstname && user.email) {
  return ok({ firstname: user.firstname, email: user.email })
}

return err(errors)
```

Not great though. The double handling of each attribute is not only verbose, it also enables contradictions if we are not careful. In this case, removing one of the checks on accident could result in `success: false` but `errors` being empty. That type of code is hard to maintain when many attributes have to be validated.

To solve this, error generation should be combined with static checking. A good way to accomplish that is to use an `Either` (or `Result`) monad for each attribute:

```TypeScript
const firstname = user.firstname
  ? ok(user.firstname) // string
  : err('firstname is required')
```

This library helps combine those results seemlessly into larger objects.

Initial goals included not being tied to this project's `Result` and providing a factory for `struct()` and `map()` to allow binding them to any implementation. Unfortunately, generating the correct types for the output requires interacting with the type variables of `Result`, which is not possible if `Result` itself is generic as TypeScript does not support higher kinded types. There is an [open issue](https://github.com/microsoft/TypeScript/issues/1213), might happen eventually.

If you are already using a different `Result` monad, feel free to copy paste the code in `api.ts` and adapt it to yours.