Система вывода типов в 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.
Внутренняя модель Vest строится вокруг функции suite, которая
принимает callback с аргументом data. Именно этот аргумент
становится источником инференса.
data.<field>.Этот процесс позволяет не объявлять интерфейсы вручную, если структура данных достаточно проста.
Хотя автоматический вывод типов удобен, в сложных системах требуется явное управление типами. 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-ориентированной, а TypeScript выполняет только статическую проверку структуры.
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;
}
Однако при конфликте полей приоритет определяется контекстом использования и явными типами.
Для повышения точности инференса применяются вспомогательные типы:
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:
Такая модель делает типизацию не вспомогательной, а интегральной частью системы описания данных, где TypeScript-инференс выступает как синхронизатор между логикой и структурой данных.