garn-validator
Ultra fast runtime type validator without dependencies.
- Supports checking primitives or objects with schemas
- Easy to use and learn but powerful
- It’s totally composable
- Fast and without dependencies
- Six behaviors:
isValidOrThrow
returns true or fails (default export)isValid
returns true or falsehasErrors
returns null or Array of errorsisValidOrLog
returns true or false and log errorisValidOrLogAll
returns true or false and log all errorsisValidOrThrowAll
returns true or throws AggregateError
- Works with ESModules or CommonJS from Node 10.x or Deno
- Works in all modern browsers
- Works in all frontend frameworks: React, Angular, Vue, etc…
Example
import is from 'garn-validator';
const isValidPassword = is(
String,
(str) => str.length >= 8,
/[a-z]/,
/[A-Z]/,
/[0-9]/,
/[-_/!·$%&/()]/
);
isValidPassword('12345Aa?'); // true
const isValidName = is(String, (name) => name.length >= 3);
isValidName('qw'); // fails
const isValidAge = is(
Number,
(age) => age > 18,
(age) => age < 40
);
isValidAge(15); // fails
// composition
const isValidUser = is({
name: isValidName,
age: isValidAge,
password: isValidPassword,
country: ["ES", "UK"], // 'ES' or 'UK'
});
isValidUser({
name: "garn",
age: 38,
password: "1234", // incorrect
country: "ES",
}); // it throws
Contents
Get started
Node
npm install garn-validator
Import with ES Modules
// default export is isValidOrThrow
import isValidOrThrow from "garn-validator";
// or use named exports
import { isValidOrThrow } from "garn-validator";
Require with CommonJs
const { isValidOrThrow } = require("garn-validator/commonjs");
// or use de default export
const isValidOrThrow = require("garn-validator/commonjs").default;
Deno
The library can be used as is in typescript
Import from deno third party modules: deno.land/x/garn_validator
// mod.ts
import is from "https://deno.land/x/garn_validator/src/index.js";
To have type definitions you can do:
import * as garnValidator from "https://deno.land/x/garn_validator/src/index.js";
import * as ValidatorTypes from "https://deno.land/x/garn_validator/src/index.d.ts";
garnValidator as typeof ValidatorTypes;
const { isValidOrThrow } = garnValidator;
Basic Usage
import is from "garn-validator"; // default export is isValidOrThrow
const isValidUser = is({ name: String, age: Number });
isValidUser({ name: "garn", age: 38 }); // true
isValidUser({ name: "garn", age: "38" }); // it throws
Check against constructor
is(Number)(2); // true
is(String)(2); // it throws
is(Array)([1, 2]); // true
is(Object)([1, 2]); // it throws
Check against primitive
is("a")("a"); // true
is(true)(false); // it throws
Check string against regex
is(/a*/)("a"); // true
is(/a/)("b"); // it throws
Check against custom function
is((value) => value > 0)(33); // true
is((value) => value > 0)(-1); // wil throw
is(Number.isNaN)(NaN); // true
is(Number.isInteger)(1.1); // wil throw
Check against enums (OR operator)
is(["a", "b"])("a"); // true
is(["a", "b"])("c"); // it throws
is([Number, String])("18"); // true
is([null, undefined, false, 0, ""])(18); // it throws
Check multiple validations (AND operator)
is(Array, (array) => array.length === 2)([1, 2]); // true
is(
(v) => v > 0,
(v) => v < 50
)(100); // it throws
Check object against an schema
const schema = { a: Number, b: Number }; // a and b are required
const obj = { a: 1, b: 2 };
is(schema)(obj); // true
is({ a: 1 })({ a: 1, b: 2 }); // true, a must be 1
is({ c: Number })({ a: 1, b: 2 }); // it throws (c is missing)
// Optional keys
is({ x$: String })({}); // true
Composable
// Simple example
const isPositive = is((v) => v > 0);
const isNotBig = is((v) => v < 100);
isPositive(-2); // it throws
is(isPositive, isNotBig)(200); // it throws
Behaviors
There are six behaviors that can be divided in two categories:
It stops in first Error (quickest):
isValidOrThrow
(returns true of throw the first error found)isValid
(returns true or false, never throws)isValidOrLog
(returns true or false and log first error, never throws)
It collects all Errors:
hasErrors
(return null or array of errors, never throws)isValidOrLogAll
(returns true or false and log all errors, never throws)isValidOrThrowAll
(returns true or throw )
The default export is isValidOrThrow
import { isValid } from "garn-validator";
// stops in first Error
isValid(/[a-z]/)("g"); // returns true
isValid(/[a-z]/)("G"); // returns false, doesn't throws
import { isValidOrLog } from "garn-validator";
// stops in first Error
isValidOrLog(/[a-z]/)("g"); // do nothing (but also returns true)
isValidOrLog(/[a-z]/)("G"); // logs error and return false
import { hasErrors } from "garn-validator";
// return null or array or errors
// checks until the end
hasErrors(/[a-z]/)("g"); // null
hasErrors(/[a-z]/, Number)("G"); // [TypeValidationError, TypeValidationError]
import { isValidOrThrowAll } from "garn-validator";
// return null or array or errors
// checks until the end
isValidOrThrowAll(/[a-z]/)("g"); // true
isValidOrThrowAll(/[a-z]/, Number)("G"); // throw AggregateError a key errors with [TypeValidationError, TypeValidationError]
Learn more at Errors
In depth
Types of validations
There are six types of validations: Primitives, Constructors, RegExp, Enums, Schemas and Custom functions
Primitives
Checking a primitive is a === comparison
Anything that is not and object in JS is a primitive: Number
, String
, undefined
, null
and Symbol
is(1)(1); // true 1 === 1
is("1")(1); // throws '1' !== 1
is(1n)(1); // throws 1n !== 1
is(undefined)(null); // throws undefined !== null
// keep in mind than a symbol is only equal to itself
let s = Symbol();
is(s)(s); // true
is(s)(Symbol()); // throws
Constructors
Checking against a constructor means to know if the value evaluated has been created from that constructor
is(Number)(2); // (2).constructor === Number --> true
is(Symbol)(Symbol()); // true
A valid constructor is a class
or any built-in constructor.
class Car {}
let honda = new Car();
is(Car)(honda); // honda.constructor === Car --> true
You can’t use a normal function used as constructor from the old JS times.
function Car(name) {
this.name = name;
}
let honda = new Car("honda");
is(Car)(honda); // throws. Car is detected as custom validator function
All built in Constructors are supported
RegExp
The perfect validator to check strings. It does what you expect:
let isLowerCased = is(/^[a-z]+$/);
isLowerCased("honda"); // /^[a-z]+$/.test('honda') --> true
// or building a regexp with the constructor RexExp;
is(new RegExp(/^[a-z]+$/))("honda"); // true
Custom function
Any function that is not a constructor is treated as custom validator.
It must return any truthy value in order to pass the validation.
is((val) => val >= 0)(10); // true
is((val) => val >= 0)(-10); // throws
is(() => "I am truthy")(10); // true
is(() => [])(10); // true
To fail a validation may return a falsy value or throw an error.
If it returns a falsy value, the default error will be thrown: TypeValidationError
If it throws an error, that error will be thrown.
is(() => false) (10); // throws TypeValidationError
is(() => 0) (10); // throws TypeValidationError
is(() => {
throw new RangeError('ups);
} ) (10); // throws RangeError
is( () => {
throw 'ups';
} ) (10); // throws 'ups'
Enums
Enums works as OR operator. Must be an array which represent all options.
let cities = ["valencia", "new york", "salzburg"];
is(cities)("valencia"); // true
is(cities)("madrid"); // throws
But it’s much more powerful than checking against primitives. It can contain any type of validator.
It checks every item until one passes.
let isNumberOrBigInt = [Number, BigInt]; // must be Number or BigInt
is(isNumberOrBigInt)(1n); // true
is(isNumberOrBigInt)(1); // true
let isFalsy = [0, "", null, undefined, false];
is(isFalsy)(""); // true
let isNumberAlike = [Number, (val) => val === Number(val)];
is(isNumberAlike)(1n); // true
is(isNumberAlike)(1); // true
is(isNumberAlike)("1"); // true
Schema
An Schema is just a plain object telling in each key which validation must pass.
let schema = {
name: String, // required and be a Number
age: (age) => age > 18, // required and be a greater than 18
tel: [Number, String], // required and be Number or String
role: ["admin", "user"], // required and be 'admin' or 'user'
credentials: {
// must be and object and will be validate with this "subSchema"
pass: String,
email: String,
},
};
let obj = {
name: "garn",
age: 20,
tel: "+34617819234",
role: "admin",
credentials: {
pass: "1234",
email: "email@example.com",
},
};
is(schema)(obj); // true
Only the keys in the schema will be checked. Any key not present in the schema won’t be checked
is({})({ a: 1 }); // true , a is not in the schema
Optional Keys
And optional key must be undefined
, null
, or pass the validation
is({ x$: Number })({ x: 1 }); // true, x is present and is Number
is({ x$: String })({ x: 1 }); // it throws, x is present but is not String
is({ x$: String })({}); // true, x is undefined
is({ x$: String })({ x: undefined }); // true, x is undefined
is({ x$: String })({ x: null }); // true, x is null
You can use key$
or 'key?'
. It would be nicer to have key?
without quotes but is not valid JS
is({ "x?": String })({}); // true
Regexp keys
You can validate multiple keys at once using a regexp key
is({
[/./]: String,
})({
a: "a",
b: "b",
}); // true
// or write it as plain string
is({
"/./": String,
})({
a: "a",
b: 1, // fails
}); // throws
// only checks the keys that matches regex
is({
[/^[a-z]+$/]: Number,
})({
x: 1,
y: 2,
z: 3,
CONSTANT: "foo", // not checked
}); // true, all lowercased keys are numbers
The required keys and optional won’t be check against a regexp key
is({
[/./]: Number,
x: String, // this required key has priority against regex key
})({
x: "x", // not checked as Number, checked as String
}); // true, x is String
is({
[/./]: Number,
x: String,
})({
x: "x", // not checked as Number, checked as String
y: "y", // checked as Number, fails
}); // throw
is({
[/./]: Number,
$x: String,
y: String,
})({
x: "x", // not checked as Number, checked as String
y: "y", // checked as String
z: 1, // checked as Number, fails
}); // throw
This feature is perfect to note that any key not specified in schema is not allowed
is({
x: String,
[/./]: () => false,
})({
x: "x",
y: "y", // fails
});
Custom validation used in schemas
When using a custom validator inside an schema will be run with 3 arguments: (value, root, keyName) => {}
- value: the value present in that key from the object
- root: the whole object, not matter how deep the validation occurs
- keyName: the name of the key to be checked.
// against root obj
is({
max: (val, root, keyName) => val > root.min,
min: (val, root, keyName) => val < root.max,
})({
max: 1,
min: -1,
}); // true
is({
max: (val, root, keyName) => val > root.min,
min: (val, root, keyName) => val < root.max,
})({
max: 10,
min: 50,
}); // it throws
// all key must be at least 3 characters
is({
[/./]: (val, root, keyName) => keyName.length > 3,
})({
max: 1, // key too short
longKey: 1, // valid key
}); // it throws, max key is too short
Validations in serie (AND operator)
The validator constructor can receive as many validations as needed.
All will be checked until one fails
const isArrayOfLength2 = is(Array, (array) => array.length === 2);
isArrayOfLength2([1, 2]); // true
is(
(v) => v > 0,
(v) => v < 50 // will fail
)(100); // it throws
const isValidPassword = is(
String, // must be an String
(str) => str.length >= 8, // and its length must be at least 8
/[a-z]/, // and must have at least one lowercase
/[A-Z]/, // and must have at least one uppercase
/[0-9]/, // and must have at least one number
/[-_/!·$%&/()]/ // and must have at least one especial character
);
isValidPassword("12345wW-"); // true
isValidPassword("12345ww-"); // fails
Errors
If a validation fails it will throw new TypeValidationError(meaningfulMessage)
which inherits from TypeError
. It can be imported.
If it throws an error from a custom validator, that error will be thrown.
import { isValidOrThrow, TypeValidationError } from "garn-validator";
try {
isValidOrThrow(Boolean)(33);
} catch (error) {
error instanceof TypeValidationError; // true
error instanceof TypeError; // true
}
try {
isValidOrThrow(() => {
throw "ups";
})(33);
} catch (error) {
error === "ups"; // true
}
try {
isValidOrThrow(() => {
throw new RangeError("out of range");
})(33);
} catch (error) {
error instanceof RangeError; // true
error instanceof TypeError; // false
}
isValidOrThrow vs isValidOrThrowAll
isValidOrThrow
will always throw the first TypeValidationError
it finds.
try {
isValidOrThrow({ a: Number, b: String })({ a: null, b: null });
} catch (error) {
error instanceof TypeValidationError; // true
error.message; // on path /a value null do not match constructor Number
}
isValidOrThrowAll
will throw an AggregateError
with all errors found.
try {
isValidOrThrowAll({ a: Number, b: String })({ a: null, b: null });
} catch (error) {
error instanceof AggregateError; // true
error instanceof SchemaValidationError; // true
console.log(error);
/*
SchemaValidationError: value {"a":null,"b":null} do not match schema {"a":Number,"b":String}
*/
console.log(error.errors);
/*
[
TypeValidationError: on path /a value null do not match constructor Number ,
TypeValidationError: on path /b value null do not match constructor String ,
]
*/
}
But if it finds only one error, it will throw TypeValidationError
, no AggregateError
try {
isValidOrThrowAll({ a: Number, b: String })({ a: null, b: "str" });
} catch (error) {
console.log(error);
/*
TypeValidationError: on path /a value null do not match constructor Number ,
*/
error instanceof TypeValidationError; // true
error instanceof SchemaValidationError; // false
}
AggregateError
There are 3 types a AggregateError
that can be thrown:
SchemaValidationError
: thrown when more than one key fails checking an schemaEnumValidationError
: thrown when all validations fails checking an enumSerieValidationError
: thrown when more than one validation fails checking an Serie
All of them inherits from AggregateError
and has a property errors with an array of all errors collected
try {
isValidOrThrowAll(Number, String)(null);
} catch (error) {
error instanceof AggregateError; // true
console.log(error.errors);
/*
[
TypeValidationError: value null do not match constructor Number ,
TypeValidationError: value null do not match constructor String ,
]
*/
}
SchemaValidationError
If using isValidOrThrowAll more than one key fails checking an Schema , it will throw a SchemaValidationError with all Errors aggregated in error.errors.
If only one key fail it will throw only that Error (not an AggregateError)
SchemaValidationError inherits from AggregateError,
But if using
isValidOrThrow
only the first Error will be thrown.
// more than 2 keys fails
try {
isValidOrThrowAll({ a: 1, b: 2 })({});
} catch (error) {
console.log(error instanceof SchemaValidationError); // true
console.log(error instanceof AggregateError); // true
console.log(error.errors.length); // 2
}
// only 1 key fails
try {
isValidOrThrowAll({ a: 1 })({});
} catch (error) {
console.log(error instanceof TypeError); // true
console.log(error instanceof SchemaValidationError); // false
}
EnumValidationError
If all validations of an enum fails, it will throw a EnumValidationError with all Errors aggregated in error.errors
But if the length of the enum is 1, it will throw only that error.
EnumValidationError inherits from AggregateError.
try {
isValidOrThrow([Boolean, String])(1);
} catch (error) {
console.log(error instanceof EnumValidationError); // true
console.log(error instanceof AggregateError); // true
}
try {
isValidOrThrow([Boolean])(1);
} catch (error) {
console.log(error instanceof EnumValidationError); // false
console.log(error instanceof TypeError); // true
}
SerieValidationError
If using isValidOrThrowAll fails all validations of a serie , it will throw a SerieValidationError with all Errors aggregated in error.errors
But if the length of the enum is 1. it will throw only this error.
SerieValidationError inherits from AggregateError.
try {
isValidOrThrowAll(Boolean, String)(1);
} catch (error) {
console.log(error instanceof SerieValidationError); // true
console.log(error instanceof AggregateError); // true
}
try {
isValidOrThrowAll(Boolean)(1);
} catch (error) {
console.log(error instanceof SerieValidationError); // false
console.log(error instanceof TypeError); // true
}
hasErrors
hasErrors
will flatMap all errors found. No AggregateError will be in the array returned.
hasErrors(/[a-z]/)("g"); // null
hasErrors(/[a-z]/, Number)("G");
/*
[
TypeValidationError: value "G" do not match regex /[a-z]/,
TypeValidationError: value "G" do not match constructor Number ,
]
*/
hasErrors({ a: Number, b: String })({ a: null, b: null });
/*
[
TypeValidationError: on path /a value null do not match constructor Number,
TypeValidationError: on path /b value null do not match constructor String
]
*/
Raw Error data
All errors the library throws has the raw data collected in a property called raw
.
try {
isValidOrThrow({ a: Number })({ a: null });
} catch (error) {
console.log(error.raw);
}
/*
{
// the validation failing
type: [Function: Number],
// the value evaluated
value: null,
// the root object
root: { a: null },
// the key failing
keyName: 'a',
// the whole path where the evaluation happens as an array
path: [ 'a' ],
// the error message
message: 'on path /a value null do not match constructor Number',
// the error constructor
'$Error': [class TypeValidationError extends TypeError],
// the behavior applied
conf: {
collectAllErrors: false,
onValid: [Function: onValidDefault],
onInvalid: [Function: onInvalidDefault]
},
}
*/
Composition in depth
You can create your own validators and use them as custom validation creating new ones.
const isPositive = isValidOrThrow((v) => v > 0);
const isNotBig = isValidOrThrow((v) => v < 100);
const isNumber = isValidOrThrow([Number, String], (num) => num == Number(num));
isValidOrThrow(isNumber, isPositive, isNotBig)('10'); // true
isValidOrThrow(isNumber, isPositive, isNotBig)(200); // it throws
When used inside another kind of behavior, it will inherit the behavior from where it has been used.
const isNotBig = isValidOrLog((v) => v < 100);
// its normal behavior
isNotBig(200); // false, logs 'value 200 do not match validator (v) => v < 100'
isValid(isNotBig)(200); // false , and won't log
isValidOrThrow(isNotBig)(200); // fails , and won't log
hasErrors(isNotBig)(200); // array, won't log
/*
[
new TypeValidationError('value 200 do not match validator (v) => v < 100')
]
*/
Actually, it’s not treated as a custom validation function. No matter is your are using hasErrors
which return null when nothing fails, and it’s just works.
const isBigNumber = hasErrors(
[Number, String],
(num) => num == Number(num),
num => num > 1000
);
// its normal behavior
isBigNumber('a12');
/* [
new TypeValidationError("value "a12" do not match validator (num) => num == Number(num)"),
new TypeValidationError("value "a12" do not match validator num => num > 1000"),
];
*/
// inherit behavior
isValidOrLog(isBigNumber)('a12'); // false, and log only one error value "a10" do not match validator (num) => num == Number(num)
Especial cases
AsyncFunction & GeneratorFunction
AsyncFunction
and GeneratorFunction
constructors are not in the global scope of any of the three JS environments (node, browser or deno). If you need to check an async function or a generator you can import them from garn-validator.
Note: Async functions and generators are not normal function, so it will fail against Function constructor
import is, { AsyncFunction, GeneratorFunction } from "garn-validator";
is(AsyncFunction)(async () => {}); // true
is(GeneratorFunction)(function* () {}); // true
is(Function)(function* () {}); // throws
is(Function)(async function () {}); // throws
arrayOf
As we use the array []
as enum, if you need to check the items of an array, you should treat it as an object and check against an schema.
import is from "garn-validator";
is(Array, { [/\d/]: Number })([1, 2, 3]); // true
is(Array, { [/\d/]: Number })([1, 2, "3"]); // throws
In order to not be so ugly you can import arrayOf
from garn-validator as a shortcut to:
export const arrayOf = type => isValid(Array, {[/^\d$/]: type})
import is, { arrayOf } from "garn-validator";
is(arrayOf(Number))([1, 2, 3]); // true
is(arrayOf(Number))([1, 2, "3"]); // throws
objectOf
You can import objectOf
from garn-validator as a shortcut to:
export const objectOf = type => isValid(Object, {[/./]: type})
import is, { objectOf } from "garn-validator";
is(objectOf(Number))({ a: 1, b: 2 }); // true
is(objectOf(Number))({ a: 1, b: "2" }); // throws
Roadmap
- Check value by constructor
- Enum type
- Shape type
- Custom validation with a function (value, root, keyName)
- Check RegEx
- Match object key by RegEx
- Multiples behaviors
- ArrayOf & objectOf
- Multiples validations
isValid(String, val => val.length > 3, /^[a-z]+$/ )('foo')
- Schema with optionals key
{ 'optionalKey?': Number }
or{ optionalKey$: Number }
- Setting for check all keys (no matter if it fails) and return (or throw) an array of errors
- Support for deno
- Support for browser
- Behavior applyDefaultsOnError. (syntax
is(Number).or(0)
) - Async validation support
- More built-in utils functions (containsText, startWith, endsWith, min, max, isLowercase, isUppercase, …)