Skip to content

zeluisping/itz

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


itz is a TypeScript library for performing runtime object type validation, while offering correct intellisense, it allows you to write custom type validations, giving you the hability to validate complex type structures.

Besides being able to validate fields, you can also mutate them, and even give back a completely different type from what came in.

If you really wanted you could even go as far as making HTTP requests (or anything else really) inside a validator and use the result of such an operation as the validated value. For cases like this don't forget that any time spent doing such extra work is extra time spent validating the object. Also when any field fails validation, all previous already validated fields are discarded, throwing away any extra fancy processing you may want to perform.

Index

Example Usage

A simple example usege would be performing validation on request data.

For example, here we quickly validate req.params to ensure it matches the structure we want. In this case we'll be wanting two fields:

  • name that must be a string
  • age that should be convertible to a number and if not then undefined (for being optional)
import { Router } from 'express';
import itz from 'itz';

const router = Router();
export default router;

const GreetParams = itz.A({
    name: itz.String,
    age: itz.AsOptionalNumber, // same as: itz.Optional(itz.AsNumber)
});

router.use('/greet/:name/:age?', (req, res) => {
    const params = GreetParams(req.params);
    if (typeof params === 'undefined') {
        res.status(401).send('Have you been drinking?');
        return;
    }
    if (typeof params.age === 'undefined') {
        res.status(200).send(`Hello there ${params.name}! How old are you?`);
        return;
    }
    if (params.age < 18) {
        res.status(403).send(`You're too young to be here ${params.name}!`);
        return;
    }
    res.status(200).send(`Hello there ${params.name}! You're ${params.age}, not too shabby.`);
});

Here the declared local variable params is of the type undefined | { name: string, age: number | undefined }. If validation fails undefined is returned, otherwise it returns the new validated object.

Custom Validators

The signature of a validator must comply with the type Validator All you need to write a custom validator is to create a function that follows the type Validator. Following is an example validator that validates a string literal type union:

import { InvalidValue, ValidatorReturn } from 'itz';

export type ESpan = 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
function isESpan(x: any): x is ESpan {
    switch (x) {
        case 'hourly':
        case 'daily':
        case 'weekly':
        case 'monthly':
        case 'yearly':
            return true;
        default:
    }
    return false;
}

export function itzESpan(key: string, value: any): ValidatorReturn<ESpan> {
    if (isESpan(value) === true) {
        return [true, value];
    }
    return InvalidValue;
}

The way a validator is written, allows us to do post processing on the value before returning it into the final object. With this you can also do more advanced processing of any kind; you can go as crazy as making an HTTP request to a remote server for retrieving a default value, anything goes. When doing such crazy things remember that if a field further ahead, from the one your validator is currently working on, fails to be validated, the whole partial object that had already been validated will be discarded as the main porpuse of this library is to validate the object against the whole wanted structure.

The return type of a validator is a tuple with either of the forms:

  • [false] on failure (use constant InvalidValue)
  • [false,undefined] @deprecated alias for [false]
  • [true, T] on success where T is the type you're validating

For simplicity, checkout Constants

  • the first element is the ok flag; true means validated and false means failure
  • the second element has two options
    • when ok === false it must be undefined
    • of type T (the type you're validating) when ok === true; alternative to doing return [false,undefined], creating tuples all the time, you can just use the constant return itz.INVALID_VALUE. For optional validators instead of always having do do return [true,undefined], also creating tuples all time, you can just do return itz.OPTIONAL_DEFAULT.

The key being validated is provided for any case where it might be useful, only the keys from the given structure are validated, any extra keys the object being validated might have are just ignored.

itz.A

This function is what constructors a validator from a structure you define. The constructed validator takes in any object whose type extends { [K: string]: any } and either returns the validated object with the correct type derived from the structure, or undefined if validation failed.

Signature

A<T extends IStructure>(
        structure: T,
    ): (what: { [K: string]: any }) => { [K in keyof T]: ValidatorType<T[K]> } | undefined

Example

An example usage of this function can be seen in [Usage Example]<(#Usage-Example).

Constants

To aid in the creation of custom validators, a few constants where created for common validation results.

InvalidValue

Defined simply as readonly [false] this constant can be used by validators when validation fails, making it more expressive.

OptionalValue

Defined simply as readonly [true, undefined] this constant is meant to be used with optional validators, when validation fails and so the fallback optional value undefined is used.

Validators

Validators are the foundation of this library, as they are the ones in charge of all the type checking, value conversions and what not.

Primitives

Primitives directly represent the primitives of the language. They're the most strict type of validators as the input value must have exactly the type we want, and nothing more.

For each of these primitives there is an optional version with the naming convention itz.OptionalTYPE; each of them is equivalente to itz.Optional(itz.TYPE).

itz.Boolean

This validator ensures that the type of the value is specifically boolean, failing if not.

A field that passes this validation will have the type boolean.

itz.Number

This validator ensures that the type of the value is specifically number, failing if not.

A field that passes this validation will have the type number.

itz.String

This validator ensures that the type of the value is specifically string, failing if not.

A field that passes this validation will have the type string.

itz.Object

This validator ensures that the type of the value is specifically object, failing if not. Despite that typeof null === 'object', this validator does not consider null to be a valid value.

A field that passes this validation will have the type object.

itz.Null

This validator ensures that the value is specifically null, failing if not.

A field that passes this validation will have the type null.

itz.Undefined

This validator ensures the value is specifically undefined, failing if not.

A field that passes this validation will have the type undefined.

itz.Any

This validator always passes. The resulting field will be of type any.

Converters

Converters are a special kind of validators. Well not really special as both have the same signatures and a similar porpuse. The difference being that these validators usually give back a completely new value derived from the original.

For each of these converters there is an optional version with the naming convention itz.AsOptionalTYPE; each of them is equivalente to itz.Optional(itz.AsTYPE).

itz.AsBoolean

Attempts to convert the value to a boolean, failing if not known how.

Conversions are possible from the following types:

  • boolean (no conversion)
  • number
    • false when 0
    • true when anything but 0
  • string
    • true when "1" or "true"
    • false when "0" or "false"

A field that passes this validation will have the type boolean.

itz.AsDate

Attempts to convert the value to a Date, failing if not known how.

Conversions are possible from the following types:

  • Date (no conversion)
  • number @see Unix Timestamp

    An integer value representing the number of milliseconds since January 1, 1970, 00:00:00 UTC (the Unix epoch), with leap seconds ignored.

  • string @see Timestamp String

    A string value representing a date, specified in a format recognized by the Date.parse() method ...

A field that passes this validation will have the type Date.

itz.AsNumber

Attempts to convert the value to a number, failing if not known how.

Conversions are possible from the following types:

  • number (no conversion)
  • string fails if not a number representation (NaN)
  • boolean
    • 1 when true
    • 0 when false

A field that passes this validation will have the type number.

itz.AsString

Attempts to convert the value to a string, failing if not known how.

Conversions are possible from the following types:

  • string (no conversion)
  • number (string representation of the number)
  • boolean
    • "true" when true
    • "false" when false

A field that passes this validation will have the type string.

Generics

The porpuse of these generics is to give you a lot more flexibility in defining your validation structures. Since they're generic, they can be used together with other kinds of validators as to compose a new type validation. Essentially they are but different composition methods.

itz.Default

This generic makes any other validator always have a fallback value, taking over validators that result in either an InvalidValue or OptionalValue. This means any field with a default will never fail nor have the type undefined.

Signature

function <T, X extends Exclude<T, undefined>>(
    validator: Validator<T>,
    Default: X,
)

Any field with this generic is guaranteed to always return the value of type T (the type being validated).

itz.Either

This generic allows you to validate against multiple types, imagine of it as something like a type union.

Signature

// Some helping types
type ValidatorArray<T extends any> = Array<Validator<T>>;
type ValidatorArrayInfer<T extends ValidatorArray<any>> = T extends ValidatorArray<infer R> ? R : never;

// The actual signature
itzEither<R extends ValidatorArray<any>>(...rest: R): Validator<ValidatorArrayInfer<R>>

Any field with this genric will return any of the value types the given validators give back. Thus having a field with itz.Either(itz.String, itz.Number) will result in a value of type string | number.

itz.Optional

This genric allows you to represent an optional field; it never fails as it always fallsback to a default value undefined.

Signature

itzOptional<T>(validator: Validator<T>): Validator<T | undefined>

The resulting value type for a field with a itz.Optional is always either the value type for the given validator or undefined.