Типизация suite

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

В основе системы типизации лежит представление 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 и параметризация данных

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

Результат выполнения 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) внутри suite

Функция test является ключевым элементом, связывающим поле данных, сообщение об ошибке и функцию проверки.

Её тип можно представить следующим образом:

function test<
  K extends keyof DataType
>(
  field: K,
  message: string,
  callback: () => void
): void;

Такой подход обеспечивает несколько уровней контроля:

  1. field всегда существует в DataType
  2. callback не может изменять структуру данных
  3. сообщение об ошибке привязано к конкретному тесту

Типизация enforce и цепочек проверок

enforce использует обобщённую типизацию значения, позволяя 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

Связь между suite и частичной типизацией (Partial Data)

В реальных сценариях данные формы часто бывают частичными. Для этого используется Partial<DataType>.

const suite = create<Partial<MyData>>("form", (data) => {
  if (data.username) {
    test("username", "Invalid", () => {
      enforce(data.username!).isString();
    });
  }
});

Типизация здесь усложняется:

  • поля становятся опциональными
  • необходимо учитывать undefined
  • enforce требует сужения типа

TypeScript обеспечивает контроль через 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;
};

Такой подход позволяет:

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

Типизация динамических тестов

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)[];
}

Типизация conditional suite и зависимых проверок

В сложных сценариях 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

Функция create способна автоматически выводить тип DataType при правильной интеграции:

const suite = create("form", (data: MyData) => {});

Однако более продвинутая типизация позволяет избежать явного указания типа:

const suite = create<MyData>("form", (data) => {
  // data автоматически типизирован как MyData
});

Это снижает дублирование и повышает согласованность модели.


Типизация расширений suite

Архитектура позволяет расширять suite через плагины или дополнительные методы, сохраняя типовую безопасность.

type ExtendedSuite<T> = Suite<T> & {
  addRule: (name: string, fn: () => void) => void;
};

Каждое расширение обязано сохранять связь с DataType, чтобы не нарушать целостность проверки.


Итоговая модель типизации suite как системы ограничений

Типизация suite формирует замкнутую систему:

  • DataType определяет структуру данных
  • keyof DataType определяет допустимые поля тестов
  • enforce зависит от типов значений
  • результат suite строго отражает входную модель
  • контекст расширяет поведение без нарушения базовой типизации

Эта система обеспечивает согласованность между данными, логикой проверки и результатом, исключая целый класс ошибок на этапе компиляции.