Типизация результатов

В основе работы 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;
  };
};

Такой подход обеспечивает:

  • автокомплит ключей формы
  • контроль соответствия схемы и результата
  • отсутствие “лишних” или “пропущенных” полей

Инференс результата через define

При использовании 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;
};

Это позволяет отслеживать жизненный цикл валидации, особенно при частых перезапусках (например, при вводе в форме).


Обобщённая модель результата Vest

В итоговой форме тип результата можно представить как композицию нескольких слоёв:

type VestResultModel<T, Meta = unknown> = {
  valid: boolean;
  fields: DeepResult<T>;
  errors: Record<keyof T, string[]>;
  warnings: Record<keyof T, string[]>;
  state: SuiteState;
  meta?: Meta;
};

Такая структура обеспечивает:

  • строгую связь с моделью данных
  • детализированный доступ к результатам тестов
  • расширяемость без потери типовой безопасности
  • предсказуемое поведение при интеграции с UI и API слоями