Вывод типов

Система вывода типов в Vest опирается на тесную интеграцию с TypeScript и концепцию декларативных валидаторов, в которых структура правил одновременно является источником как runtime-логики, так и статической типизации. Основная задача механизма вывода типов заключается в том, чтобы минимизировать расхождение между описанием схемы валидации и типами данных, используемыми в приложении.


Базовая модель типизации

Типизация в Vest формируется из структуры набора правил (suite). Каждый suite представляет собой декларацию полей и связанных с ними проверок. TypeScript анализирует структуру этих правил и выводит тип объекта данных автоматически.

Ключевой принцип: каждое поле, участвующее в валидации, становится частью результирующего типа модели данных.

Простейший пример:

import { suite, test } from 'vest';

const loginSuite = suite('login', (data) => {
  test('username', 'required', () => {
    if (!data.username) {
      return 'Username is required';
    }
  });

  test('password', 'required', () => {
    if (!data.password) {
      return 'Password is required';
    }
  });
});

TypeScript выводит тип входного объекта как:

{
  username: string;
  password: string;
}

Даже при отсутствии явных аннотаций тип данных формируется из контекста использования data.


Вывод типов из контекста suite

Внутренняя модель Vest строится вокруг функции suite, которая принимает callback с аргументом data. Именно этот аргумент становится источником инференса.

Механизм инференса

  1. Анализируется тело callback-функции suite.
  2. Собираются обращения к data.<field>.
  3. Каждый доступ к свойству регистрируется как потенциальное поле модели.
  4. Формируется объединённый тип объекта.

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


Явная типизация и расширение инференса

Хотя автоматический вывод типов удобен, в сложных системах требуется явное управление типами. Vest поддерживает передачу generics в suite, что позволяет переопределить или уточнить вывод.

type LoginData = {
  username: string;
  password: string;
  rememberMe?: boolean;
};

const loginSuite = suite<LoginData>('login', (data) => {
  test('username', 'required', () => {
    if (!data.username) {
      return 'Required';
    }
  });
});

В этом случае происходит приоритет явного типа над выводимым, но система продолжает проверять согласованность использования полей.


Типизация полей и ошибок

Одной из ключевых особенностей Vest является связывание имени поля с типом ошибок. Каждое правило test возвращает либо undefined, либо строку ошибки, либо более сложный объект ошибки в расширенных конфигурациях.

Базовый тип ошибки

type ValidationResult = string | undefined;

На уровне TypeScript выводится структура:

Record<string, ValidationResult[]>

Каждое поле может иметь несколько правил, что приводит к массиву ошибок.


Инференс на основе условной логики

TypeScript-инференс в Vest ограничен статическим анализом, однако библиотека использует структурные соглашения для повышения точности.

Пример условного поля:

test('discountCode', 'invalid', () => {
  if (data.hasCoupon) {
    if (!data.discountCode) {
      return 'Required';
    }
  }
});

TypeScript не всегда способен вывести условную обязательность поля discountCode. Поэтому используется расширение типов через явные generics или вспомогательные типы.


Кросс-полевой вывод типов

В сложных сценариях типизация должна учитывать взаимосвязи полей. Vest позволяет описывать зависимости, но TypeScript не всегда может выразить их напрямую.

Пример зависимости

test('confirmPassword', 'mismatch', () => {
  if (data.password !== data.confirmPassword) {
    return 'Passwords do not match';
  }
});

Типовая модель при этом остаётся:

{
  password: string;
  confirmPassword: string;
}

Однако логическая связь между полями фиксируется только на уровне runtime-валидации.


Инференс асинхронных правил

Асинхронные проверки в Vest добавляют дополнительный слой сложности для типизации.

test('username', 'taken', async () => {
  const exists = await api.checkUsername(data.username);
  if (exists) {
    return 'Username is taken';
  }
});

TypeScript рассматривает такие тесты как возвращающие Promise<ValidationResult>, что приводит к следующей модели:

Promise<string | undefined>

Система объединяет синхронные и асинхронные результаты в единый поток ошибок.


Типизация вложенных структур

При работе с вложенными объектами вывод типов становится иерархическим. Vest поддерживает обращение к глубоко вложенным полям через точечную нотацию.

test('profile.email', 'invalid', () => {
  if (!data.profile.email.includes('@')) {
    return 'Invalid email';
  }
});

TypeScript выводит вложенную структуру:

{
  profile: {
    email: string;
  }
}

Однако вложенность требует аккуратного управления, так как чрезмерная глубина может приводить к потере точности инференса.


Ограничения вывода типов

Несмотря на мощную интеграцию с TypeScript, система вывода типов в Vest имеет ряд ограничений:

  • невозможность полного анализа динамических ключей (data[fieldName]);
  • ограниченная поддержка условной обязательности полей;
  • отсутствие полноценного вывода бизнес-логики из runtime-условий;
  • сложность точного вывода типов при глубокой композиции suites.

Эти ограничения обусловлены тем, что система валидации остаётся runtime-ориентированной, а TypeScript выполняет только статическую проверку структуры.


Композиция suites и влияние на типы

Vest позволяет объединять несколько suites, что влияет на итоговый тип данных.

const baseSuite = suite('base', (data) => {
  test('id', 'required', () => {
    if (!data.id) return 'Required';
  });
});

const profileSuite = suite('profile', (data) => {
  test('name', 'required', () => {
    if (!data.name) return 'Required';
  });
});

При композиции типы объединяются:

{
  id: string;
  name: string;
}

Однако при конфликте полей приоритет определяется контекстом использования и явными типами.


Расширение системы типов через утилитарные конструкции

Для повышения точности инференса применяются вспомогательные типы:

  • Partial моделей для режимов редактирования
  • Required для финальной валидации
  • Pick и Omit для разбиения схем
type DraftUser = Partial<User>;
type FinalUser = Required<User>;

Vest интегрируется с этими конструкциями, позволяя адаптировать схему валидации под разные стадии жизненного цикла данных.


Типизация контекста выполнения

Некоторые реализации suites используют дополнительный контекст выполнения (например, внешние сервисы или параметры среды). Это добавляет ещё один слой типизации:

suite('checkout', (data, ctx) => {
  test('payment', 'invalid', () => {
    if (!ctx.paymentGateway) {
      return 'No gateway';
    }
  });
});

TypeScript выводит тип контекста как отдельный generic параметр, что позволяет строго типизировать зависимости от внешних ресурсов.


Связь вывода типов и архитектуры схем

Типы в Vest не являются изолированным слоем. Они напрямую отражают архитектуру validation-suites:

  • структура suite → структура данных;
  • набор test → набор ограничений;
  • контекст → расширение модели;
  • композиция → объединение типов.

Такая модель делает типизацию не вспомогательной, а интегральной частью системы описания данных, где TypeScript-инференс выступает как синхронизатор между логикой и структурой данных.