Валидационные правила в системе Vest строятся как декларативные функции, объединённые в тестовые сценарии (suite). При использовании JavaScript типизация часто остаётся неявной, однако при переходе на TypeScript становится критически важной для предотвращения ошибок на уровне компиляции, особенно в сложных формах, где набор правил зависит от состояния данных.
Типизация правил решает три ключевые задачи: предсказуемость входных данных, строгую связь между полями и их валидаторами, а также корректное определение результата проверки без необходимости ручного приведения типов.
Каждое правило в Vest по сути представляет функцию, принимающую значение и возвращающую результат проверки. В простейшем виде это можно выразить так:
type Rule<T> = (value: T) => boolean;
Однако такая модель слишком упрощена, поскольку в реальной системе валидации требуется учитывать контекст ошибки, вложенные проверки и асинхронные сценарии.
Более приближённый к практике вариант:
type RuleResult = true | string;
type Rule<T> = (value: T) => RuleResult;
Здесь строка интерпретируется как сообщение об ошибке, а
true означает успешное прохождение проверки.
В Vest правила обычно объединяются в тесты внутри suite. Типизация должна сохранять связь между полем и его типом.
type FormSchema = {
username: string;
age: number;
};
Далее определяется тип теста:
type TestFn<T> = (value: T) => void;
Однако в контексте Vest тест не просто функция, а декларация, которая регистрируется в suite, поэтому добавляется контекст выполнения:
type TestContext<T> = {
value: T;
field: string;
};
И итоговая сигнатура:
type VestTest<T> = (ctx: TestContext<T>) => RuleResult;
Главная сложность заключается в том, чтобы связать структуру формы с набором правил.
type Suite<T> = {
[K in keyof T]: VestTest<T[K]>[];
};
Такое отображение гарантирует, что для каждого поля формы будет применён корректный тип значения.
Пример использования:
const suite: Suite<FormSchema> = {
username: [
({ value }) => value.length > 3 || "Слишком короткое имя"
],
age: [
({ value }) => value >= 18 || "Возраст должен быть не менее 18"
]
};
Система Vest возвращает результат проверки, который также должен быть типизирован.
type FieldResult = {
valid: boolean;
errors: string[];
};
Для всей формы:
type ValidationResult<T> = {
[K in keyof T]: FieldResult;
};
Это позволяет статически гарантировать, что результат валидации полностью соответствует структуре входных данных.
Асинхронные проверки — частый сценарий (например, проверка уникальности username через API). В Vest это требует расширения базового типа правила.
type AsyncRule<T> = (value: T) => Promise<RuleResult> | RuleResult;
Для унификации:
type MaybePromise<T> = T | Promise<T>;
type AsyncVestTest<T> = (ctx: TestContext<T>) => MaybePromise<RuleResult>;
Типизация suite должна учитывать оба варианта:
type AnyVestTest<T> = VestTest<T> | AsyncVestTest<T>;
При создании кастомных правил важно сохранять типовую строгость. Например, правило проверки строки на формат email:
type Email = string & { __brand: "email" };
Функция-валидатор:
const isEmail = (value: string): value is Email => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
};
И типизированное правило:
const emailRule: VestTest<string> = ({ value }) =>
isEmail(value) || "Некорректный email";
Такой подход позволяет постепенно вводить номинальные типы поверх структурных.
При масштабировании логики важно автоматизировать построение типов.
type InferSuiteResult<T> = ValidationResult<T>;
Для строгой связи можно использовать mapped types:
type InferRules<T> = {
[K in keyof T]: AnyVestTest<T[K]>[];
};
Это создаёт единый контракт между данными и правилами проверки.
Внутри Vest каждый тест получает контекст выполнения, который может включать дополнительные поля: зависимости, состояние других полей формы, флаги выполнения.
type ExtendedContext<T, K extends keyof T> = {
value: T[K];
field: K;
get: <P extends keyof T>(field: P) => T[P];
};
Это позволяет безопасно обращаться к связанным значениям:
const passwordRule: VestTest<FormSchema["password"]> = ({ value, get }) =>
value === get("confirmPassword") || "Пароли не совпадают";
Типизация также помогает контролировать побочные эффекты. Если правило должно быть чистым:
type PureRule<T> = (value: T) => RuleResult;
Если допускается асинхронность или обращение к внешнему состоянию:
type EffectRule<T> = (ctx: TestContext<T>) => MaybePromise<RuleResult>;
Разделение таких типов позволяет явно различать уровни сложности правил и управлять архитектурой валидации.
Типы должны поддерживать композицию:
type ComposedRule<T> = PureRule<T> | EffectRule<T>;
Функции комбинирования сохраняют тип:
const and =
<T>(...rules: ComposedRule<T>[]): ComposedRule<T> =>
(ctx) => {
for (const rule of rules) {
const result = typeof rule === "function" ? rule(ctx as any) : true;
if (result !== true) return result;
}
return true;
};
Наиболее важный эффект типизации в Vest заключается в том, что структура формы, набор правил и результат валидации начинают образовывать замкнутую систему типов. Любое несоответствие между полем и его проверкой выявляется на этапе компиляции, а не во время выполнения.
Такой подход уменьшает количество скрытых ошибок и делает систему валидации предсказуемой даже при значительном росте сложности бизнес-логики.