quartet
v10.2.2
Published
functional and convenient validation library
Downloads
153
Maintainers
Readme
Quartet 10
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
Examples
See examples here.
Is there extra word in this list?
- 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!
Objections
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.
Confession
This is exactly what this library provides you. I hope this example inspires you to read further and subsequently start using this library.
How to use it?
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.
Primitives
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.
Schemas out of the box
Quartet provides pre-defined schemas for specific checks. They are in the properties of the v
compiler function.
v.any: Schema
const checkBoolean = v(v.any);
// same as
const checkBoolean = () => true;
v.array: Schema
const checkBoolean = v(v.array);
// same as
const checkBoolean = (value) => Array.isArray(v);
v.boolean: Schema
const checkBoolean = v(v.boolean);
// same as
const checkBoolean = (x) => typeof x === "boolean";
v.finite: Schema
const checkFinite = v(v.finite);
// same as
const checkFinite = (x) => Number.isFinite(x);
v.function: Schema
const checkFunction = v(v.function);
// same as
const checkFunction = (x) => typeof x === "function";
v.negative: Schema
const checkNegative = v(v.negative);
// same as
const checkNegative = (x) => x < 0;
v.never: Schema
const checkNegative = v(v.never);
// same as
const checkNegative = () => false;
v.number: Schema
const checkNumber = v(v.number);
// same as
const checkNumber = (x) => typeof x === "number";
v.positive: Schema
const checkPositive = v(v.positive);
// same as
const checkPositive = (x) => x > 0;
v.safeInteger: Schema
const checkSafeInteger = v(v.safeInteger);
// same as
const checkSafeInteger = (x) => Number.isSafeInteger(x);
v.string: Schema
const checkString = v(v.string);
// same as
const checkString = (x) => typeof x === "string";
v.symbol: Schema
const checkSymbol = v(v.symbol);
// same as
const checkSymbol = (x) => typeof x === "symbol";
Schemas created using quartet methods
The compiler function also has methods that return schemas.
v.and(...schemas: Schema[]): Schema
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;
};
v.arrayOf(elemSchema: Schema): Schema
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;
};
v.custom(checkFunction: (x: any) => boolean, description?: string): Schema
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.)
v.max(maxValue: number, isExclusive?: boolean): Schema
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;
v.maxLength(maxLength: number, isExclusive?: boolean): Schema
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) });
v.min(minValue: number, isExclusive?: boolean): Schema
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);
v.minLength(minLength: number, isExclusive?: number): Schema
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) });
v.not(schema: Schema): Schema
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)));
v.pair(keyValueSchema: Schema): Schema
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.
v.test(tester: { test(x: any) => boolean }): Schema
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);
Variant schemas
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;
};
The schema for an object is an object
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,
});
Conclusions
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.
Explanations
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,
},
]
*/
Predefined Instances
There is two predefined instances of quartet:
import { v } from "quartet"; // Zero-configured instance, without explanations
import { e } from "quartet"; // Instance with explanations.
Advanced Quartet
// TODO: Write it!
Ajv vs Quartet 10
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)