Строгая типизация enforce

В архитектуре Vest типизация строится вокруг идеи предсказуемого вывода результата проверки и строгого описания правил валидации через цепочки декларативных вызовов. Ключевым элементом, влияющим на типобезопасность правил, выступает enforce — слой утверждений, который формирует единый механизм проверки значений и их соответствия ожиданиям.

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

Типизация в Vest опирается на три уровня:

  • тип входных данных (input schema на уровне логики формы или объекта)
  • тип правил проверки (validation suite)
  • тип результата (validation result)

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

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

Механика вывода типов в suite

При объявлении набора правил проверок типизация начинает формироваться автоматически:

suite('user', () => {
  test('email', 'Email is invalid', () => {
    enforce(values.email).isNotBlank();
  });

  test('age', 'Age is invalid', () => {
    enforce(values.age).greaterThan(18);
  });
});

В контексте TypeScript каждый test становится источником вывода типа для соответствующего поля. Если поле используется в нескольких проверках, его тип объединяется через пересечение ограничений.

enforce как ядро типобезопасных утверждений

enforce — это не просто рантайм-утверждение. В типизированной модели Vest он выступает как функция, возвращающая строго типизированный объект-assertion.

Каждый метод цепочки:

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

Пример:

enforce(value).isString().longerThan(3);

Здесь происходит последовательное сужение типа:

  1. isString() — гарантирует, что значение рассматривается как string
  2. longerThan(3) — добавляет числовое ограничение, не изменяя базовый тип, но уточняя допустимый диапазон

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

Каждый метод enforce возвращает новый типизированный контекст:

  • исходный тип: unknown
  • после isNumber(): number
  • после isNotEmpty(): number | string с ограничением непустого значения
  • после matches(regex): строка с дополнительным constraint

Такой подход позволяет TypeScript выполнять статический контроль корректности цепочек:

  • невозможность вызова строковых методов на числовом контексте
  • запрет логически несовместимых проверок
  • строгая проверка порядка вызовов в цепочке

Генерическая модель enforce

Внутренне enforce использует generics для сохранения исходного типа:

function enforce<T>(value: T): EnforceAssertion<T>

Каждый метод возвращает новый тип:

isString(): EnforceAssertion<string>
isNumber(): EnforceAssertion<number>

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

Типизация пользовательских правил

Пользовательские проверки интегрируются в систему через расширение enforce:

enforce.extend({
  isEven(value: number) {
    return value % 2 === 0;
  }
});

Типизация таких расширений требует явного указания сигнатур:

  • входной тип
  • возвращаемый boolean
  • контекст применения

После расширения метод становится частью типизированной цепочки:

enforce(value).isNumber().isEven();

Если метод применён к несовместимому типу, TypeScript фиксирует ошибку ещё до выполнения.

Типизация ошибок и сообщений

Каждое правило валидации связано с типом ошибки:

  • string — текстовое сообщение
  • object — структурированная ошибка
  • array — набор ошибок по подполям

Пример типизации ошибки:

test('password', 'Too weak password', () => {
  enforce(values.password).longerThan(8);
});

Тип результата связывает поле password с типом ошибки string.

Инференс результата проверки

Результат выполнения suite представляет собой объект, где каждая ошибка строго ассоциирована с полем:

{
  email: string | undefined;
  age: string | undefined;
}

TypeScript выводит этот тип автоматически на основе структуры test-ов. Если поле не имеет ошибок, значение становится undefined.

Уточнение типов через условные проверки

В более сложных сценариях enforce используется для формирования условных типов:

enforce(value)
  .isString()
  .when(v => v.startsWith('A'), e => e.longerThan(5));

Здесь тип сужается в зависимости от условия, и TypeScript фиксирует возможные ветвления:

  • базовый тип string
  • подтип с дополнительным ограничением длины

Композиция enforce в сложных схемах

При работе с вложенными объектами типизация распространяется рекурсивно:

test('user.address.city', 'Invalid city', () => {
  enforce(values.user.address.city).isString();
});

Типовая модель автоматически строит путь user.address.city, сохраняя строгую связь между вложенностью данных и результатом проверки.

Ограничения строгой типизации

Несмотря на высокий уровень статической проверки, существуют ограничения:

  • динамические ключи ослабляют вывод типов
  • вычисляемые поля требуют явного указания generic-типа
  • сложные условные конструкции могут приводить к расширению union-типов

Тем не менее система сохраняет предсказуемость благодаря централизованному механизму вывода через enforce.

Поведение при несовпадении типов

Если тип значения не соответствует цепочке enforce, происходит:

  • либо ошибка компиляции (в TypeScript-контексте)
  • либо runtime-ошибка при выполнении assertion

Пример несовместимости:

enforce(value).isNumber().longerThan(3);

Метод longerThan недоступен для числового типа в строгой модели, что фиксируется на этапе компиляции.

Типизация как часть архитектуры валидации

В рамках Vest строгая типизация через enforce не является дополнительным слоем, а встроена в ядро системы. Она определяет:

  • структуру правил
  • допустимые операции над значениями
  • форму результата проверки
  • поведение расширений

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