В основе работы Vest лежит декларативное описание правил валидации, которое компилируется в структурированный результат выполнения. Этот результат представляет собой не просто набор ошибок, а строго типизированное дерево состояния проверки, отражающее состояние каждого поля, группы и всего набора правил.
Типизация результатов в Vest играет ключевую роль в TypeScript-ориентированных проектах, поскольку позволяет связать логику валидации с интерфейсами данных, исключая рассинхронизацию между формой и схемой проверки.
Результат выполнения валидационного набора (suite) представляет собой объект, содержащий агрегированное состояние всех проверок:
Упрощённая логическая структура выглядит следующим образом:
type SuiteResult = {
valid: boolean;
errors: Record<string, string[]>;
warnings?: Record<string, string[]>;
tests: Record<string, FieldResult>;
};
На практике структура более детализирована, поскольку Vest хранит результаты каждого теста, а не только итоговые ошибки.
Каждое поле в Vest описывается отдельным объектом состояния. Это позволяет агрегировать несколько проверок в единый результат.
type FieldResult = {
valid: boolean;
errors: string[];
warnings: string[];
tests: TestResult[];
};
Каждый тест (test case) фиксирует результат конкретного правила:
type TestResult = {
description?: string;
valid: boolean;
error?: string;
warning?: string;
};
Такой подход позволяет сохранять не только итог, но и историю проверок, что критично для сложных форм.
Наиболее важной частью типизации является привязка результата валидации к исходной структуре данных формы.
type FormData = {
email: string;
password: string;
age: number;
};
Vest позволяет описывать результат как отображение ключей формы:
type VestResult<T> = {
valid: boolean;
errors: Record<keyof T, string[]>;
fields: {
[K in keyof T]: FieldResult;
};
};
Такой подход обеспечивает:
При использовании create и test функции
Vest тип результата может быть выведен автоматически, если используется
TypeScript-ориентированное описание схемы.
import { create, test } from "vest";
const suite = create((data: FormData) => {
test("email", "Email is required", () => {
if (!data.email) return "Email is empty";
});
});
Тип результата связывается с ключами
"email" | "password" | "age" на основе контекста выполнения
тестов.
Одним из ключевых аспектов является унификация типа ошибки. В Vest ошибка может быть:
type ErrorType = string | undefined;
type ErrorsMap<T> = Record<keyof T, ErrorType[]>;
Однако более строгая модель избегает undefined:
type StrictErrors<T> = Record<keyof T, string[]>;
Такой подход упрощает обработку результата в UI-слое, так как отсутствие ошибок выражается пустым массивом.
При работе со сложными формами (например, объектами с вложенностью) результат становится иерархическим.
type Profile = {
user: {
email: string;
credentials: {
password: string;
};
};
};
Тип результата отражает структуру данных:
type DeepResult<T> = {
[K in keyof T]: T[K] extends object
? DeepResult<T[K]>
: FieldResult;
};
Это позволяет сохранять согласованность между формой и результатом без ручного описания всех уровней.
Внутренне Vest может хранить результаты тестов в плоской структуре, однако для UI часто требуется нормализованная форма.
Пример нормализации:
type NormalizedResult<T> = {
valid: boolean;
errors: {
[K in keyof T]?: string;
};
};
Или расширенная версия:
type NormalizedDetailedResult<T> = {
valid: boolean;
byField: Record<keyof T, {
valid: boolean;
messages: string[];
}>;
};
Нормализация особенно важна при интеграции с формами React или Vue, где требуется прямое сопоставление поля и ошибки.
Vest предоставляет набор функций для анализа результата, и их типизация также важна для безопасного использования.
type IsValidFn<T> = (field: keyof T) => boolean;
type GetErrorsFn<T> = (field: keyof T) => string[];
type HasErrorsFn<T> = (field?: keyof T) => boolean;
Эти функции опираются на результат выполнения и используют типизацию ключей формы для предотвращения ошибок обращения.
При использовании асинхронных проверок результат становится зависимым от Promise-цепочек.
type AsyncSuiteResult<T> = Promise<VestResult<T>>;
Каждый тест может возвращать Promise:
test("email", "Email already exists", async () => {
const exists = await api.checkEmail(data.email);
if (exists) return "Email is taken";
});
Типизация гарантирует, что асинхронные ошибки корректно агрегируются в итоговом объекте результата.
Vest позволяет расширять результат дополнительной информацией, что требует корректной типизации расширяемого объекта.
type ExtendedResult<T, Meta> = VestResult<T> & {
meta?: Meta;
};
Пример:
type MetaInfo = {
duration: number;
timestamp: number;
};
type ResultWithMeta = ExtendedResult<FormData, MetaInfo>;
Такой подход используется для:
Ключевым принципом является связь между именами тестов и ключами результата.
test("email", "Invalid email", () => {});
Строковый литерал "email" становится основой для:
errors.email;
fields.email;
При строгой типизации это выражается через:
type FieldName<T> = keyof T & string;
и использование обобщений в API тестирования.
Дополнительно результат включает состояние выполнения набора тестов:
type SuiteState = {
isRunning: boolean;
completed: boolean;
aborted: boolean;
};
Это позволяет отслеживать жизненный цикл валидации, особенно при частых перезапусках (например, при вводе в форме).
В итоговой форме тип результата можно представить как композицию нескольких слоёв:
type VestResultModel<T, Meta = unknown> = {
valid: boolean;
fields: DeepResult<T>;
errors: Record<keyof T, string[]>;
warnings: Record<keyof T, string[]>;
state: SuiteState;
meta?: Meta;
};
Такая структура обеспечивает: