Skip to content
/ fefe Public

Validate, sanitize and transform values with proper TypeScript types and zero dependencies.

License

Notifications You must be signed in to change notification settings

paperhive/fefe

Repository files navigation

fefe

npm version Test status codecov

Validate, sanitize and transform values with proper TypeScript types and with a single dependency (fp-ts).

🔎  Validation: checks a value (example: check if value is string)
🔩  Sanitization: if a value is not valid, try to transform it (example: transform value to Date)
🛠️  Transformation: transforms a value (example: parse JSON)
🔌  Everything is a function: functional approach makes it easy to extend – just plug in your own function anywhere!
↔️  Based on Either: explicit and type-safe error handling – left path is a (typed!) error, right path is a valid value (see below).

Installation

npm install fefe

Usage

🔎 Validation example

Validation checks the provided value and returns it with proper types.

import { object, string } from 'fefe'

const validatePerson = object({ name: string() })

const result = validatePerson({ name: 'Leia' })
if (isFailure(result)) {
  return console.error(result.left)

// result is of type { name: string }
const person = result.right

☝️ You can also use fefe to define your types easily:

import { ValidatorReturnType } from 'fefe'
type Person = ValidatorReturnType<typeof validatePerson> // { name: string }

⚙️ Basic transformation example

Parse a value

In this example a string needs to be parsed as a Date. You can use pipe() to pass a value through multiple functions:

import { object, parseDate, pipe, string, ValidatorReturnType } from 'fefe'

const sanitizeMovie = object({
  title: string(),
  releasedAt: pipe(string()).pipe(parseDate())
})

// { title: string, releasedAt: Date }
type Movie = ValidatorReturnType<typeof sanitizeMovie>

const movie: Movie = sanitizeMovie({
  title: 'Star Wars',
  releasedAt: '1977-05-25T12:00:00.000Z'
})

Then movie.right equals { title: 'Star Wars', releasedAt: Date(1977-05-25T12:00:00.000Z) } (releasedAt now is a date).

Note: Chaining functions can also be achieved by the standard functional tools like flow and chain in fp-ts.

Parse a value on demand (sanitize)

Sometimes a value might already be of the right type. In the following example we use union() to create a sanitizer that returns a provided value if it is a Date already and parse it otherwise. If it can't be parsed either the function will throw:

import { date, parseDate, pipe, union } from 'fefe'

const sanitizeDate = union(
  date(),
  pipe(string()).pipe(parseDate())
)

🛠️ Complex transformation example

This is a more complex example that can be applied to parsing environment variables or query string parameters. Again, we use pipe to compose functions. Here, we also add a custom function that splits a string into an array.

import { object, parseJson, pipe, string, success } from 'fefe'

const parseConfig = object({
  gcloudCredentials: pipe(string())
    .pipe(parseJson())
    .pipe(object({ secret: string() })),
  whitelist: pipe(string()
    .pipe(secret => success(str.split(',')))
})

// { gcloudCredentials: { secret: string }, whitelist: string[] }
type Config = ValidatorReturnType<typeof parseConfig>

const config: Config = parseConfig({
  gcloudCredentials: '{"secret":"foobar"}',
  whitelist: 'alice,bob'
})

Then config.right will equal { gcloudCredentials: { secret: 'foobar'}, whitelist: ['alice', 'bob'] }.

Documentation

Transformer

A transformer is a function that accepts some value of type V (it could be unknown) and returns a type T:

type Transform<T> = (v: V) => Result<T>

The result can either be a FefeError (see below) or the validated value as type T:

type Result<T> = Either<FefeError, T>

fefe uses the Either pattern with types and functions from fp-ts. Either can either represent an error (the "left" path) or the successfully validated value (the "right" path). This results in type-safe errors and explicit error-handling. Example:

import { isFailure } from 'fefe'

const result: Result<string> = ...
if (isFailure(result)) {
  console.error(result.left)
  process.exit(1)
}
const name = result.right

You may wonder why fefe does not just throw an error and the answer is:

  1. Throwing an error is a side-effect which goes against pure functional programming.
  2. Lack of type-safety: A thrown error can be anything and needs run-time checking before it can be used in a meaningful way.

You can read more about it here.

For simplifying the transition from a 2.x codebase you can use the toThrow(t: Transformer<V, T>) function that returns a funtion (v: V) => T that returns the value directly and throws instead of returning a FefeError in the case of an error. Note that the thrown FefeThrowError has a different structure than the pre-3.x FefeError.

Validator

A validator is just a special (but common) case of a transformer where the input is unknown:

type Validator<T> = Transformer<unknown, T>

FefeError

fefe validators return a FefeError if a value can't be validated/transformed. Note that FefeError is not derived from the JavaScript Error object but is a simple object.

If an error occurs it will allow you to pinpoint where exactly the error(s) occured and why. The structure is the following:

type FefeError = LeafError | BranchError

LeafError

A LeafError can be seen as the source of an error which can happen deep in a nested object and it carries both the value that failed and a human-readable reason describing why it failed.

interface LeafError {
  type: 'leaf'
  value: unknown
  reason: string
}

BranchError

A BranchError is the encapsulation of one or more errors on a higher level.

interface BranchError {
  type: 'branch'
  value: unknown
  childErrors: ChildError[]
}

interface ChildError {
  key: Key
  error: FefeError
}

Imagine an array of values where the values at position 2 and 5 fail. This would result in two childErrors: one with key equal to 2 and key equal to 5. The error property is again a FefeError so this is a full error tree.

getErrorString(error: FefeError): string

To simplify handling of errors, you can use getErrorString() which traverses the tree and returns a human-readable error message for each LeafError – along with the paths and reasons.

Example error message: user.id: Not a string.

array(elementValidator, options?): Validator<T[]>

Returns a validator that checks that the given value is an array and that runs elementValidator on all elements. A new array with the results is returned as Result<T[]>.

Options:

  • elementValidator: Validator<T>: validator that is applied to each element. The return values are returned as a new array.
  • options.minLength?: number, options.maxLength?: number: restrict length of array
  • options.allErrors?: boolean: set to true to return all errors instead of only the first.

boolean(): Validator<boolean>

Returns a validator that returns value if it is a boolean and returns an error otherwise.

date(options?): Validator<Date>

Returns a validator that returns value if it is a Date and returns an error otherwise.

Options:

  • options.min?: Date, options.max?: Date: restrict date

discriminatedUnion(key, definition, options?): Validator<ObjectResult<D>>

Returns a validator that returns value if:

  • it is an object and
  • the value[key] is a key of definition
  • value (without key) passes the validation as specified in definition[key]. Otherwise it returns an error. A new object is returned that has the results of the validator functions as values.

Options: see object().

enumerate(value1, value2, ...): Validator<value1 | value2 | ...>

Returns a validator that returns value if if equals one of the strings value1, value2, .... and returns an error otherwise.

mapObjectKeys(map): Transformer<S, T>

Returns a transformer that takes the input object and returns a new object with the keys of map. For each key k the resulting object's value is the value for the key map[k] of the input object.

Options:

  • map: Record<string, keyof S>: maps output object keys to input object keys.

This function is very useful in combination with object():

const validateEnv = pipe(
    object({
      FOO: string(),
      BAR: optional(pipe(string()).pipe(parseNumber())),
    })
  )
  .pipe(mapObjectKeys({ foo: 'FOO', bar: 'BAR' }))

const result = validatEnv({ FOO: 'str', BAR: '1337' })

Then isSuccess(result) will be true and result.right equals to { foo: 'str', bar: 1337 }.

number(options?): Validator<number>

Returns a validator that returns value if it is a number and returns an error otherwise.

Options:

  • options.min?: number, options.max?: number: restrict number
  • options.integer?: boolean: require number to be an integer (default: false)
  • options.allowNaN?: boolean, options.allowInfinity?: boolean: allow NaN or infinity (default: false)

object(definition, options?): Validator<ObjectResult<D>>

Returns a validator that returns value if it is an object and all values pass the validation as specified in definition, otherwise it returns an error. A new object is returned that has the results of the validator functions as values.

Options:

  • definition: ObjectDefinition: an object where each value is a Validator<T>.
  • allowExcessProperties?: boolean: allow excess properties in value (default: false). Excess properties are not copied to the returned object.
  • allErrors?: boolean: set to true to return all errors instead of only the first (default: false).

You can use the following helpers:

  • optional(validator: Validator<T>): generates an optional key validator with the given validator.
  • defaultTo(validator: Validator<T>, default: D | () => D: generates a validator that defaults to default() if it is a function and default otherwise.

objectMap(valueValidator, options?): Validator<{ [k: string]?: T }>

Returns a validator that returns a map with value type T if all values pass the valueValidator, otherwise it returns an error. A new object is returned that has the results of the validator functions as values.

Options:

  • valueValidator: Validator<T>: validator that is applied to each value.
  • options.allErrors?: boolean: set to true to return all errors instead of only the first.

pipe(validator1: Transformer<A, B>): Pipe<A, B>

Returns a transformer that offers a .pipe(validator2: Transformer<B, C>): Pipe<A, C> method.

string(options?): Validator<string>

Returns a validator that returns value if it is a string and returns an error otherwise.

Options:

  • options.minLength?: number, options.maxLength?: number: restrict length of string
  • options.regex?: RegExp: require string to match regex

union(validator1, validator2, ...): Validator<A | B | ...>

Returns a validator that returns the return value of the first validator called with value that does not return an error. The function returns an error if all validators return an error. All arguments are validators (e.g., validator1: Validator<A>, validator2: Validator<B>, ...)

parseBoolean(): Transformer<string, boolean>

Returns a transformer that parses a string as a boolean.

parseDate(options?): Transformer<string, Date>

Returns a transformer that parses a string as a date.

Options:

  • options.iso?: boolean: require value to be an ISO 8601 string.

parseJson(): Transformer<string, unknown>

Returns a transformer that parses a JSON string. Since parsed JSON can in turn be almost anything, it is usually combined with another validator like object({ ... }).

parseNumber(): Transformer<string, number>

Returns a transformer that parses a number string.