Когда я создавал библиотеку для валидации данных quartet
, взял следующие цели-ориентиры:
- TypeScript
- Краткость и простота
- Производительность
В этой статье покажу как библиотека quartet
понимает, что значат слова "Краткий" и "Простой" в контексте подхода к валидации.
Как работает quartet
?
Когда он нужен? Он нужен тогда, когда нужна валидация данных
Что такое валидация? Проверка данных на соответствия требованиям. Чаще всего типам.
Как quartet
в этом помогает? Он создает функцию валидации на основании декларативного описания требований — схемы. Функция-компилятор v
преобразует схемы в функции валидации. Также внутри v
хранятся заготовленные схемы и методы для создания более сложных схем. Детальнее смотрите в документации.
Как это работает?
- Пишешь схему
- Даешь ее на вход функции-компилятору
v
чтобы получить функцию валидации. - Функция валидаци готова к использованию!
Давайте с помощью quartet отвалидируем все данные не напрягая мозг в семь подходов.
Начнём с легкого и постепенно будем увеличивать сложность требований.
Валидация типа с единственным примитивным значением
Например:
type Answer = 42;
Схема этого типа в quartet
:
const answerSchema = 42;
Чтобы получить его функцию-валидатор пишем код:
import { v } from "quartet";
const checkAnswer = v(42);
Всё просто. Схема примитива — он сам.
Валидация встроенных типов
Предположим мы хотим валидировать не конкретную строку, а все строки. Или не одно конкретное число — а все числа.
Напишем пару валидаторов для примитивных типов. Посмотрим, что в них общее, а что нет.
const checkNumber = (x) => typeof x === "number";
const checkString = (x) => typeof x === "string";
const checkBoolean = (x) => typeof x === "boolean";
const checkSymbol = (x) => typeof x === "symbol";
// ...
У них одна структура:
const checkSomeType = x => typeof x === "<type>", где <type> – нужный нам тип
Итого для каждого примитивного типа, у нас есть схема:
Тип | Схема |
---|---|
'string' |
v.string |
'number' |
v.number |
'boolean' |
v.boolean |
'symbol' |
v.symbol |
Итак, мы можем получить функцию валидации числа таким образом:
const checkNumber = v(v.number);
// то же, что
const checkNumber = (x) => typeof x === "number";
Функции в quartet
валидируются таким же способом
Тип | Схема |
---|---|
'function' |
v.function |
На практике это выглядит так:
const checkFunction = v(v.function);
// то же, что
const checkFunction = (x) => typeof x === "function";
Всё просто. Cхема типа — одноименный метод с префиксом v.
.
Операция «Или» для схем
Предположим у нас есть такой тип:
type NullableString = string | null;
Схема этого типа в quartet
:
const nullableStringSchema = [v.string, null];
А функция валидации получим написав так:
const checkNullableString = v([v.string, null]);
В общем виде можно представить в виде типа:
type VariantSchema = Schema[];
Всё просто. Схема валидации вариантов – массив схем.
Операция «И» для схем
Допустим у нас есть объект у которого
type Password = string // Пароль должен быть длиннее 8-ми символов и содержать как минимум 1 букву и цифру.
Опишем эти требования с помощью схем. Для такого случая есть методы v.minLength()
и v.test(RegExp)
const stringSchema = v.string;
const min8Schema = v.minLength(8);
const atLeastOneDigitSchema = v.test(/\d/);
const atLeastOneLetterSchema = v.test(/[A-Za-z]/);
Чтобы объединить схемы у нас есть метод v.and()
:
const passwordSchema = v.and(
stringSchema,
min8Schema,
atLeastOneDigitSchema,
atLeastOneLetterSchema,
)
Всё просто. Для нескольких проверок используй v.and(схема, схема, ...)
.
Валидация интерфейсов
Предположим есть интерфейс объекта Person
:
interface Person {
name: string
age: number
}
Что нужно знать, чтобы выполнить проверку обьекта на соответствие интерфейсу Person
?
Необходимо знать, что у него есть поля с именами name
и age
. И они должны быть типа string
и number
. И больше ничего!
Таким образом нам нужно иметь возможность перечислить, какие у объекта должны быть поля, и какие схемы им соответствуют.
Наиболее подходящей для этого структурой данных является — объект!
Итак схема выглядит так:
const personSchema = {
name: v.string,
age: v.number,
};
В общем виде схему интерфейса можно представить так:
interface ObjectInterfaceSchema {
[propertyName: string]: Schema;
}
Всё просто. Схема валидации интерфейса объекта — объект, в котором ключи — это поля объекта, а значения — схемы валидации.
Валидация масивов
Допустим у нас масив чисел:
type Numbers = number[];
Для этого у нас есть метод v.arrayOf()
. На вход метода идёт схема елемента.
const checkNumbers = v(v.arrayOf(v.number));
Всё просто. Для валидации масива используй v.arrayOf(схема елемента)
.
Пользовательская валидация
А что если мы хотим встроить свою валидацию в схему quartet
.
Давайте создадим схему проверки на парность числа. Для єтого напишем пользовательскую схему с помощью метода v.custom(своя функция валидации)
:
const evenSchema = v.custom((x) => x % 2 === 0)
const evenNumberSchema = v.and(
v.number,
evenSchema,
);
Всё просто. Для своих проверок используй v.custom(своя функция валидации)
.
Выводы
- Схема примитива — он сам.
- Cхема типа — одноименный метод с префиксом
v.
. - Схема валидации вариантов — массив схем.
- Для нескольких проверок используй
v.and(схема, схема, ...)
. - Схема валидации интерфейса объекта — объект, в котором ключи — это поля объекта, а начения — схемы валидации.
- Для валидации масива используй
v.arrayOf(схема елемента)
. - Для своих проверок используй
v.custom(своя функция валидации)
.
P.S.
Я работаю в компании. Внедрение библиотеки quartet
было так:
Поначалу разработчики подходили и спрашивали как написать проверку того или иного случая. После 4 вопросов они легко пишут валидации, которые им нужны.
Интересно послушать про ваш опыт работы с валидацией данных, напишите в комментариях об этом!
mzhuravlev61
Вопрос насчет explanations, сейчас получить данные ошибок валидации можно только из поля экземпляра валидатора IContext.explanations. Выглядит не очень безопасно и не ФПшно. Понимаю, что так сделано потому, что type guard это не настоящий возвращаемый тип, а TS-магический, то есть нельзя сделать функцию с сигнатурой вроде (s: unknown) => [s is string, s]. Есть ли мысли, как это можно красиво решить?
Эта библиотека https://github.com/gcanti/io-ts, например, выдает результат проверки как Either<ИскомыйТип, ТипОшибки>, но в этом случае не используются type guards.
andrewbeletskiy Автор
Спасибо, что так вдумчиво отнеслись к моему труду. Попытаюсь ответить на ваши предложения и вопросы.
ajv
(29M скачиваний в неделю) использует подобный подход к сообщениям об ошибке. Аргумент от авторитета, и мне не стыдно))А если серьёзно, какого рода небезопасности вы предполагаете? очень интересно узнать.
ФП — не синоним простоты, хотя концепция очень стройная и прекрасная.
Недостатки такой сигнатуры обсуждались здесь. Я с их аргументами согласен.
Используете
v2
вместоv
— везде где эта абстракция вам нужна.У такого подхода с обертками есть два плюса, которые я считаю важными:
1) Вам не нужно беспокоится, что в quartet абстракция не такая мощная, или например — что она не моноид, а вам обязательно нужно, чтобы результаты валидаций — можно было конкатенировать.
2) Тем, которым ваша абстракция не по душе — не обязаны её использовать, и quartet их не заставляет.
Поэтому то quartet и сделан так, что даёт вам свободу выбора абстракции.
А базовая абстракция c Type Guards, по моему опыту часто вполне достаточна и наиболее легко воспринимается.
Хорошая штука, но отнюдь, не очень простая