Size: 5.39 KB (minified and gzipped). No dependencies. Size Limit controls the size.
It is a declarative and fast tool for data validation.
- Quartet 10
- Examples
- Is there extra word in this list?
- Objections
- Confession
- How to use it?
- Primitives
- Schemas out of the box
- Schemas created using quartet methods
v.and(...schemas: Schema[]): Schema
v.arrayOf(elemSchema: Schema): Schema
v.custom(checkFunction: (x: any) => boolean, description?: string): Schema
v.max(maxValue: number, isExclusive?: boolean): Schema
v.maxLength(maxLength: number, isExclusive?: boolean): Schema
v.min(minValue: number, isExclusive?: boolean): Schema
v.minLength(minLength: number, isExclusive?: number): Schema
v.not(schema: Schema): Schema
v.pair(keyValueSchema: Schema): Schema
v.test(tester: { test(x: any) => boolean }): Schema
- Variant schemas
- The schema for an object is an object
- Conclusions
- Explanations
- Predefined Instances
- Advanced Quartet
- Ajv vs Quartet 10
See examples here.
- 3rd-party API
- Typescript
- Confidence
- Simplicity
- Performance
In our opinion, there is no extra word. Let's take a look at the following situation.
We request information about the user from the 3rd-party API.
This data has a certain type, we write it in the TypeScript language in this way:
interface Response {
user: {
id: number;
name: string;
age: number;
gender: "Male" | "Female";
friendsIds: number[];
};
}
To achieve Confidence we will write a function that tells us whether the answer is of type Response
.
// More details about such functions google
// "Typescript Custom Type Guards"
function checkResponse(response: any): response is Response {
if (response == null) return false;
if (response.user == null) return false;
if (typeof response.user.id !== "number") return false;
if (typeof response.user.name !== "string") return false;
if (typeof response.user.age !== "number") return false;
if (VALID_GENDERS_DICT[response.user.gender] !== true) return false;
if (!response.user.friendsIds || !Array.isArray(response.user.friendsIds))
return false;
for (let i = 0; i < response.user.friendsIds.length; i++) {
const id = response.user.friendsIds[i];
if (typeof id !== "number") return false;
}
return true;
}
const VALID_GENDERS_DICT = {
Male: true,
Female: true,
};
Now in the place where we make the request, we will check:
// ...
const userResponse = await GET("http://api.com/user/1");
if (!checkResponse(userResponse)) {
throw new Error("API response is invalid");
}
const { user } = userResponse; // has type Response
// ...
This approach, with a stretch, but can be called Simple.
It's pretty hard to come up with a faster option to provide a type guarantee. Therefore, this code has sufficient Performance.
We got everything we wanted!
You can say: How can you call the function checkResponse
simple?
We would agree if it were as declarative as the type of Response
itself. Something like:
const checkResponse = v<Response>({
user: {
id: v.number,
name: v.string,
age: v.number,
gender: ["Male", "Female"],
friendsIds: v.arrayOf(v.number),
},
});
Yes! Anyone would agree with that. Such an approach would be extremely convenient. But only on condition that the performance remains at the same level as the imperative version.
This is exactly what this library provides you. I hope this example inspires you to read further and subsequently start using this library.
Here's what you need to do to use this library.:
- Install
npm i -S quartet
- Import the "compiler" of schemas:
import { v } from "quartet";
- Describe the type of value you want to check.
This step is optional, and if you do not use TypeScript, you can safely skip it. It just helps you write a validation scheme.
type MyType = // ...
- Create a validation scheme
const myTypeSchema = // ...
- Compile this schema into a validation function
const checkMyType = v<MyType>(myTypeSchema);
or the same without TypeScript type parameter:
const checkMyType = v(myTypeSchema);
- Use
checkMyType
on data that you are not sure about. It will returntrue
if the data is valid. It will returnfalse
if the data is not valid.
Each primitive Javascript value is its own validation scheme.
I will give an example:
const isNull = v(null);
// same as
const isNull = (x) => x === null;
or
const is42 = v(42);
// same as
const is42 = (x) => x === 42;
Primitives are all Javascript values, with the exception of objects (including arrays) and functions. That is: undefined
,null
, false
,true
, numbers (NaN
,Infinity
, -Infinity
including) and strings.
Quartet provides pre-defined schemas for specific checks. They are in the properties of the v
compiler function.
const checkBoolean = v(v.any);
// same as
const checkBoolean = () => true;
const checkBoolean = v(v.array);
// same as
const checkBoolean = (value) => Array.isArray(v);
const checkBoolean = v(v.boolean);
// same as
const checkBoolean = (x) => typeof x === "boolean";
const checkFinite = v(v.finite);
// same as
const checkFinite = (x) => Number.isFinite(x);
const checkFunction = v(v.function);
// same as
const checkFunction = (x) => typeof x === "function";
const checkNegative = v(v.negative);
// same as
const checkNegative = (x) => x < 0;
const checkNegative = v(v.never);
// same as
const checkNegative = () => false;
const checkNumber = v(v.number);
// same as
const checkNumber = (x) => typeof x === "number";
const checkPositive = v(v.positive);
// same as
const checkPositive = (x) => x > 0;
const checkSafeInteger = v(v.safeInteger);
// same as
const checkSafeInteger = (x) => Number.isSafeInteger(x);
const checkString = v(v.string);
// same as
const checkString = (x) => typeof x === "string";
const checkSymbol = v(v.symbol);
// same as
const checkSymbol = (x) => typeof x === "symbol";
The compiler function also has methods that return schemas.
It creates a kind of connection schemas using a logical AND (like the operator &&
)
const positiveNumberSchema = v.and(v.number, v.positive);
const isPositiveNumber = v(positiveNumberSchema);
// same as
const isPositiveNumber = (x) => {
if (typeof x !== "number") return false;
if (x <= 0) return false;
return true;
};
According to the element scheme, it creates a validation scheme for an array of these elements:
const elemSchema = v.and(v.number, v.positive);
const arraySchema = v.arrayOf(elemSchema);
const checkPositiveNumbersArray = v(arraySchema);
// same as
const checkPositiveNumbersArray = (x) => {
if (!x || !Array.isArray(x)) return false;
for (let i = 0; i < 0; i++) {
const elem = x[i];
if (typeof elem !== "number") return false;
if (elem <= 0) return false;
}
return true;
};
From the validation function, it creates a schema.
function checkEven(x) {
return x % 2 === 0;
}
const evenSchema = v.custom(checkEven);
const checkPositiveEvenNumber = v(v.and(v.number, v.positive, evenSchema));
// same as
const checkPositiveEvenNumber = (x) => {
if (typeof x !== "number") return false;
if (x <= 0) return false;
if (!checkEven(x)) return false;
return true;
};
If description is passed it will be placed inside explanation if such is used.
import { e } from "quartet";
const isEven = (x) => x % 2 === 0;
const evenNumbersValidator = e(
e.arrayOf(
e.custom(isEven, "should be even")
)
);
evenNumbersValidator([]) // true
evenNumbersValidator([1]) // false
evenNumbersValidator.explanations
// [
// {
// value: 1,
// schema: {
// type: 'Custom',
// description: 'should be even',
// innerExplanations: []
// },
// path: [ 0 ],
// innerExplanations: []
// }
// ]
(See Advanced Quartet for more.)
By the maximum (or boundary) number returns the corresponding validation scheme.
const checkLessOrEqualToFive = v(v.max(5));
// same as
const checkLessOrEqualToFive = (x) => x <= 5;
const checkLessThanFive = v(v.max(5, true));
// same as
const checkLessThanFive = (x) => x < 5;
By the maximum (or boundary) value of the length, returns the corresponding schema.
const checkTwitterText = v(v.maxLength(140));
// same as
const checkTwitterText = (x) => x != null && x.length <= 140;
const checkTwitterText = v({ length: v.max(140) });
const checkSmallArray = v(v.maxLength(20, true));
// same as
const checkSmallArray = (x) => x != null && x.length < 140;
const checkTwitterText = v({ length: v.max(20, true) });
By the minimum (or boundary) number returns the corresponding validation scheme.
const checkNonNegative = v(v.min(0));
// same as
const checkNonNegative = (x) => x >= 0;
const checkPositive = v(v.min(0, true));
// same as
const checkPositive = (x) => x > 0;
const checkPositive = v(v.positive);
By the minimum (or boundary) value of the length, returns the corresponding schema.
const checkLargeArrayOrString = v(v.minLength(1024));
// same as
const checkLargeArrayOrString = (x) => x != null && x.length >= 1024;
const checkLargeArrayOrString = v({ length: v.min(1024) });
const checkNotEmptyStringOrArray = v(v.minLength(0, true));
// same as
const checkNotEmptyStringOrArray = (x) => x != null && x.length > 0;
const checkNotEmptyStringOrArray = v({ length: v.min(0, true) });
Applies logical negation (like the !
Operator) to the passed schema. Returns the inverse schema to the passed one.
const checkNonPositive = v(v.not(v.positive));
const checkNot42 = v(v.not(42));
const checkIsNotNullOrUndefined = v(v.and(v.not(null), v.not(undefined)));
It's a method that returns a special kind of Schema that can be used only as a single parameter of v.arrayOf
and [v.rest]
prop in object schema.
It is used to get access to index or prop name of validated value.
keyValueSchema
is a schema that should validate an object with two props key
(in which index or prop name will be stored) and value
(in which value will be stored)
The main goal is to validate dictionaries.
const validPersonsNames = ["Andrew", "Vasilina", "Bohdan", "TF"];
const checkPhoneBook = v({
[v.rest]: {
key: validPersonsNames,
value: v.string,
},
});
checkPhoneBook({}); // will be true
checkPhoneBook({
Andrew: "0975017374",
Vasilina: "23123123",
}); // will be true
checkPhoneBook({
NotAnAndrew: "0975017374",
Vasilina: "23123123",
}); // will be false
Example with v.arrayOf
const isSquaresOfIndices = v.arrayOf(
v.pair(v.custom(({ key, value }) => value === key * key))
);
isSquaresOfIndices([]); // true
isSquaresOfIndices([0]); // true, because 0 = 0 * 0
isSquaresOfIndices([0, 1]); // true, because 1 = 1 * 1
isSquaresOfIndices([0, 1, 4]); // true, because 4 = 2 * 2
isSquaresOfIndices([0, 1, 4, 10]); // false, because 10 !== 3 * 3
const checkShortArrayOfStrings = v(v.and(v.arrayOf(v.string), v.minLength(10)));
// the same as
const checkShortArrayOfStrings = v(
v.arrayOf(
v.pair({
key: v.max(9),
value: v.string,
})
)
);
Good to mention that:
const valueSchema = v.string;
const checkPhoneBook = v({
[v.rest]: valueSchema,
});
// is the same as
const checkPhoneBook = v({
[v.rest]: v.pair({
value: valueSchema,
}),
});
WARNING: there is only two ways to use v.pair according to its rules:
const schemaOfArray = v.arrayOf(v.pair(...))
const schemaWithRest = {
// ...
[v.rest]: v.pair(...)
}
Any other usage either will throw error or will have undefined behavior.
On an object with the test
method, returns a schema that checks whether the given test
method returns true
on the checked value .
Most commonly used with Regular Expressions.
v.test(tester) === v.custom(x => tester.test(x))
const checkIntegerNumberString = v(v.test(/[1-9]\d*/));
// same as
const checkIntegerNumberString = (x) => /[1-9]\d*/.test(x);
An array of schemas acts as a connection of schemas using the logical operation OR (operator ||
)
const checkStringOrNull = v([v.string, null]);
// same as
const checkStringOrNull = (x) => {
if (typeof x === "string") return true;
if (x === null) return true;
return false;
};
const checkGender = v(["male", "female"]);
// same as
const VALID_GENDERS = { male: true, female: true };
const checkStringOrNull = (x) => {
if (VALID_GENDERS[x] === true) return true;
return false;
};
const checkRating = v([1, 2, 3, 4, 5]);
// same as
const checkRating = (x) => {
if (x === 1) return true;
if (x === 2) return true;
if (x === 3) return true;
if (x === 4) return true;
if (x === 5) return true;
return false;
};
An object whose values are schemas is an object validation schema. Where the appropriate fields are validated by the appropriate schemas.
const checkHelloWorld = v({ hello: "World" });
// same as
const checkHelloWorld = (x) => {
if (x == null) return false;
if (x.hello !== "World") return false;
return true;
};
If you want to validate objects with previously unknown fields, use v.rest
interface PhoneBook {
[name: string]: string;
}
const checkPhoneBook = v({
[v.rest]: v.string,
});
The scheme from the v.rest
key will validate all unspecified fields.
interface PhoneBookWithAuthorId {
authorId: number;
[name: string]: string;
}
const checkPhoneBookWithAuthorId = v({
authorId: v.number,
[v.rest]: v.string,
});
Using these schemes and combining them, you can declaratively describe validation functions, and the v
compiler function will create a function that imperatively checks the value against your scheme.
If you need explanations of validation just use e
instance instead of v
instance.
import { e as v } from "quartet";
const checkPerson = v({
name: v.string,
});
checkPerson({ name: 1 }); // false
checkPerson.explanations;
/*
[
{
path: ["name"],
schema: {
type: "String",
},
value: 1,
},
]
*/
There is two predefined instances of quartet:
import { v } from "quartet"; // Zero-configured instance, without explanations
import { e } from "quartet"; // Instance with explanations.
// TODO: Write it!
I wrote a benchmark in order to compare one of the fastest ajv
validation libraries with my example from the introduction.
const Benchmark = require("benchmark");
const { v } = require("quartet");
const validator = v({
user: {
id: v.number,
name: v.string,
age: v.number,
gender: ["Male", "Female"],
friendsIds: v.arrayOf(v.number),
},
});
const Ajv = require("ajv");
const ajv = new Ajv();
const ajvValidator = ajv.compile({
type: "object",
required: ["user"],
properties: {
user: {
type: "object",
required: ["id", "name", "age", "gender", "friendsIds"],
properties: {
id: { type: "number" },
name: { type: "string" },
age: { type: "number" },
gender: { type: "string", enum: ["Male", "Female"] },
friendsIds: { type: "array", items: { type: "number" } },
},
},
},
});
const positive = [
{
user: {
id: 1,
name: "Andrew",
age: 23,
gender: "Male",
friendsIds: [2, 3],
},
},
{
user: {
id: 2,
name: "Vasilina",
age: 20,
gender: "Female",
friendsIds: [1],
},
},
{ user: { id: 3, name: "Bohdan", age: 23, gender: "Male", friendsIds: [1] } },
{ user: { id: 4, name: "Siroja", age: 99, gender: "Male", friendsIds: [] } },
];
const negative = [
null,
false,
undefined,
{},
{ user: null },
{ user: false },
{ user: undefined },
{
user: {
id: "1",
name: "Andrew",
age: 23,
gender: "Male",
friendsIds: [2, 3],
},
},
{
user: {
id: 1,
name: undefined,
age: 23,
gender: "Male",
friendsIds: [2, 3],
},
},
{
user: {
id: 1,
name: "Andrew",
age: undefined,
gender: "Male",
friendsIds: [2, 3],
},
},
{
user: {
id: 1,
name: "Andrew",
age: 23,
gender: undefined,
friendsIds: [2, 3],
},
},
{
user: {
id: 1,
name: "Andrew",
age: 23,
gender: "male",
friendsIds: [2, 3],
},
},
{
user: {
id: 1,
name: "Andrew",
age: 23,
gender: "Male",
friendsIds: undefined,
},
},
];
const suite = new Benchmark.Suite();
suite
.add("ajv", function() {
for (let i = 0; i < positive.length; i++) {
ajvValidator(positive[i]);
}
for (let i = 0; i < negative.length; i++) {
ajvValidator(negative[i]);
}
})
.add("Quartet 10", function() {
for (let i = 0; i < positive.length; i++) {
validator(positive[i]);
}
for (let i = 0; i < negative.length; i++) {
validator(negative[i]);
}
})
.on("cycle", function(event) {
console.log(String(event.target));
})
.on("complete", function() {
console.log(
this.filter("fastest")
.map("name")
.toString()
);
})
.run();
And the result is this:
ajv x 1,029,338 ops/sec ±0.79% (90 runs sampled)
Quartet 9: Allegro x 3,727,212 ops/sec ±9.26% (66 runs sampled)