В основе системы типизации лежит представление suite как строго структурированного контейнера, описывающего набор проверок, контекст выполнения и результирующее состояние валидации. В TypeScript-окружении suite становится обобщённым типом, связывающим входные данные, правила проверки и формат ошибок.
Ключевая идея заключается в том, что suite параметризуется типом данных формы или модели:
type MyData = {
username: string;
age: number;
};
Далее suite получает этот тип как основу для всех внутренних операций:
import { create, test, enforce } from "vest";
const suite = create<MyData>("user_form", (data) => {
test("username", "Invalid username", () => {
enforce(data.username).isString();
});
test("age", "Invalid age", () => {
enforce(data.age).isNumber();
});
});
Тип MyData становится центральной точкой вывода типов:
поля, контекст и ошибки начинают опираться на его структуру.
Suite в типизированной модели представляет собой generic-конструкцию:
Suite<DataType, ContextType = void>
DataType — форма входных данныхContextType — дополнительный контекст выполнения
(например, пользователь, режим, конфигурация)Контекст позволяет расширять проверку без изменения основной модели данных:
type Context = {
isAdmin: boolean;
};
const suite = create<MyData, Context>("user_form", (data, ctx) => {
test("age", "Admins only rule", () => {
if (!ctx.isAdmin) {
enforce(data.age).greaterThan(18);
}
});
});
Типизация контекста обеспечивает строгую проверку доступных полей внутри suite и предотвращает использование несуществующих свойств.
Одной из наиболее значимых особенностей типизации suite является автоматическое выведение ключей объекта данных.
test("username", ...)
Здесь строковый литерал "username" не является
произвольным значением — он ограничен keyof DataType.
Это означает, что следующая конструкция становится невозможной на уровне компиляции:
test("usernmae", "Error", () => {});
// ошибка TypeScript: такого ключа нет в DataType
Таким образом обеспечивается синхронизация между моделью данных и набором тестов.
Результат выполнения suite также имеет строгую структуру, зависящую от входного типа данных.
Обобщённо результат можно представить так:
type SuiteResult<DataType> = {
hasErrors: boolean;
errors: {
[K in keyof DataType]?: string[];
};
warnings?: {
[K in keyof DataType]?: string[];
};
};
Это означает:
Пример использования:
const result = suite(data);
if (result.errors.username) {
console.log(result.errors.username);
}
Функция test является ключевым элементом, связывающим
поле данных, сообщение об ошибке и функцию проверки.
Её тип можно представить следующим образом:
function test<
K extends keyof DataType
>(
field: K,
message: string,
callback: () => void
): void;
Такой подход обеспечивает несколько уровней контроля:
field всегда существует в DataTypeenforce использует обобщённую типизацию значения,
позволяя TypeScript отслеживать допустимые операции.
enforce(data.age).greaterThan(18);
Тип enforce зависит от входного значения:
function enforce<T>(value: T): EnforceChain<T>;
Внутри EnforceChain<T> набор методов определяется
типом T:
number доступны арифметические проверкиstring — строковые проверкиboolean — логические утвержденияПример:
enforce(data.username).longerThan(3); // только если username: string
enforce(data.age).isNumber(); // применимо к number
Ошибочные вызовы исключаются на уровне компиляции:
enforce(data.age).longerThan(3);
// ошибка: number не поддерживает longerThan
В реальных сценариях данные формы часто бывают частичными. Для этого
используется Partial<DataType>.
const suite = create<Partial<MyData>>("form", (data) => {
if (data.username) {
test("username", "Invalid", () => {
enforce(data.username!).isString();
});
}
});
Типизация здесь усложняется:
undefinedTypeScript обеспечивает контроль через narrowing:
if (data.username !== undefined) {
enforce(data.username).isString();
}
Suite поддерживает вложенные структуры данных, что требует рекурсивной типизации ключей.
type Profile = {
user: {
name: string;
credentials: {
email: string;
};
};
};
Валидационные ключи могут быть строками путей:
test("user.name", "Invalid name", () => {
enforce(data.user.name).isString();
});
Типизация таких путей строится через рекурсивные типы:
type Paths<T> = T extends object
? {
[K in keyof T]: `${K & string}` | `${K & string}.${Paths<T[K]>}`;
}[keyof T]
: never;
Это позволяет ограничить строковые пути только реальными структурами данных.
Ошибки в suite типизируются как строго ассоциированные с ключами модели:
type FieldErrorMap<T> = Partial<Record<keyof T, string[]>>;
Более сложная версия поддерживает метаинформацию:
type FieldError<T> = {
field: keyof T;
message: string;
code?: string;
};
Такой подход позволяет:
Suite допускает динамическое добавление тестов, однако типизация ограничивает область допустимых полей.
Object.keys(data).forEach((key) => {
test(key as keyof MyData, "Invalid field", () => {
enforce(data[key as keyof MyData]).isDefined();
});
});
TypeScript требует явного приведения типов, так как runtime-ключи теряют статическую информацию.
Для более безопасного подхода используется вспомогательная функция:
function typedKeys<T>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
В сложных сценариях suite зависит от условий, влияющих на структуру проверки:
type Mode = "create" | "update";
type Data<ModeType extends Mode> = {
id?: ModeType extends "update" ? number : never;
name: string;
};
Suite учитывает режим:
const suite = create<Data<"update">>("form", (data) => {
test("id", "Required in update", () => {
enforce(data.id).isNumber();
});
});
TypeScript гарантирует корректность использования полей в зависимости от режима.
Функция create способна автоматически выводить тип
DataType при правильной интеграции:
const suite = create("form", (data: MyData) => {});
Однако более продвинутая типизация позволяет избежать явного указания типа:
const suite = create<MyData>("form", (data) => {
// data автоматически типизирован как MyData
});
Это снижает дублирование и повышает согласованность модели.
Архитектура позволяет расширять suite через плагины или дополнительные методы, сохраняя типовую безопасность.
type ExtendedSuite<T> = Suite<T> & {
addRule: (name: string, fn: () => void) => void;
};
Каждое расширение обязано сохранять связь с DataType, чтобы не нарушать целостность проверки.
Типизация suite формирует замкнутую систему:
Эта система обеспечивает согласованность между данными, логикой проверки и результатом, исключая целый класс ошибок на этапе компиляции.