В экосистеме TypeScript типы существуют исключительно на этапе статического анализа (compile time) и полностью стираются компилятором (Type Erasure), превращаясь в сырой JavaScript. Любые внешние входные данные — тела сетевых HTTP-запросов, полезная нагрузка Server Actions, пользовательский ввод в HTML-формах, значения параметров URL и записи в IndexedDB — поступают в рантайм в статусе unknown или any. Единственным барьером, гарантирующим безопасность типов во время выполнения, является валидация схем времени выполнения (Runtime Schema Validation).
Исторически де-факто стандартом веб-разработки стал Zod, предоставивший удобный синтаксис цепочек методов (method chaining) и глубокую интеграцию с React Hook Form. Однако развитие мобильного веба и микросервисной архитектуры выдвинуло жесткие требования к двум ключевым метрикам: размеру клиентского JavaScript-бандла (критично для Core Web Vitals) и скорости валидации десятков тысяч объектов в секунду (критично для высоконагруженных шлюзов Node.js и табличных редакторов). На этой почве сформировались две альтернативные архитектурные школы: модульный функциональный валидатор Valibot и JSON Schema JIT-компилятор TypeBox.
Выбор инструмента валидации определяется тремя архитектурными векторами:
Чтобы понять причины расхождений в производительности и размере бандла, необходимо проанализировать, как каждый движок транслирует декларативную схему в исполняемый машинный код V8.
В библиотеке Zod каждая схема является экземпляром специализированного класса, наследующегося от ZodType:
class ZodString extends ZodType<string> {
min(length: number) { ... }
email() { ... }
regex(pattern: RegExp) { ... }
}Анатомия проблемы размера бандла (Tree-Shaking Defeat):
Когда вы пишете import { z } from 'zod' и вызываете z.string(), среда исполнения подтягивает весь класс ZodString. Все методы валидации (email(), url(), uuid(), cuid(), regex(), ip(), datetime()) физически привязаны к прототипу класса или внутреннему реестру проверок. Даже если в вашем проекте используется только простая проверка длины строки z.string().min(3), сборщик модулей (Webpack, Rollup, Turbopack) не может безопасно вырезать неиспользуемые регулярные выражения для проверки кредитных карт или IPv6 адресов. В результате клиент получает в бандл около 14 КБ чистого сжатого кода парсера.
Анатомия скорости: При парсинге объекта Zod динамически проходит по дереву классов, выполняя вложенные вызовы функций _parse(), перераспределяя память под промежуточные объекты ZodParsedType и накапливая стек вызовов, что создает ощутимый оверхед на массивах из десятков тысяч сущностей.
Valibot отбрасывает классы и чейнинг в пользу функционального конвейера (Pipelines):
import { pipe, string, minLength, email } from 'valibot';
const Schema = pipe(string(), minLength(3), email());Каждая функция (string(), minLength(), email()) является независимой чистой экспортируемой функцией, не имеющей ссылок на остальные утилиты пакета. Сборщик проекта включает в бандл исключительно те функции валидации, которые были явно импортированы. Если форме нужна только проверка обязательного текста, размер рантайма валидации составит менее 600 байт.
TypeBox решает задачу валидации с противоположной стороны — он опирается на строгий стандарт спецификации JSON Schema Draft 7/2020-12. TypeBox оперирует чистыми объектами схем в оперативной памяти:
import { Type } from '@sinclair/typebox';
const UserSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
age: Type.Integer({ minimum: 18 })
});Сам по себе объект UserSchema — это чистый JSON-объект без логики исполнения:
{ type: 'object', properties: { id: { type: 'string', format: 'uuid' }, age: { type: 'integer', minimum: 18 } } }.
Для валидации TypeBox предлагает два режима:
Value.Check(schema, value)): Стандартный рекурсивный обход JSON Schema.TypeCompiler.Compile(schema)): TypeBox компилирует JSON-дерево в одну монолитную, плоскую функцию JavaScript через new Function(). Сгенерированная функция содержит инлайновые проверки if (typeof value.age !== 'number' || value.age < 18) return false без циклов, замыканий и выделения промежуточных массивов. Движок V8 оптимизирует такую функцию до машинного кода (JIT TurboFan), достигая скорости проверки свыше 50 миллионов операций в секунду.Представим процесс валидации формы как конвейер обработки данных:
[Клиентский ввод: Record<string, unknown>]
|
v
+---------------------------+
| 1. Structural Guard | Проверка типов данных (string, number, boolean, array, object)
+---------------------------+
|
v
+---------------------------+
| 2. Semantic Refinements | Бизнес-правила (min, max, email, regex, custom refinements)
+---------------------------+
|
v
+---------------------------+
| 3. Async Pipeline | Внешние I/O проверки (проверка уникальности email в БД, API)
+---------------------------+
|
v
[Типизированный валидный результат: TOutput] ИЛИ [Карта ошибок: FormErrors]Валидатор сверяет соответствие типа поля (typeof x === 'string', Array.isArray). Предотвращает обращение к свойствам undefined.
Если схема настроена на приведение типов, строковые значения из HTML FormData (например, '42') приводятся к числам (42) или датам.
Проверка диапазонов, регулярных выражений, взаимозависимостей полей (например, пароль и подтверждение пароля).
Запуск сетевых проверок с AbortSignal для отмены устаревших запросов (проверка занятости логина, доступности домена).
Генерация результирующего иммутабельного объекта или форматированного списка ошибок с указанием path и message.
Почему JIT-компилированная схема TypeBox (TypeCompiler.Compile) выполняет валидацию 10 000 объектов в разы быстрее, чем Zod.safeParse()?
Замена монолитного Zod на Valibot в публичных посадочных страницах (Landing Pages, формы оформления заказов Checkout) позволяет сократить вес критического бандла на 12–14 КБ. Это напрямую улучшает метрики Total Blocking Time (TBT) и Largest Contentful Paint (LCP) на слабых мобильных устройствах.
В финансовых системах, где клиент загружает CSV/Excel таблицу на 50 000 строк для проведения массовых выплат, валидация через TypeCompiler.Compile() занимает 18 мс, тогда как Zod требует более 1200 мс, вызывая перегрузку Event Loop в Node.js.
Zod остается непревзойденным стандартом для форм со сложной условной логикой («если выбран тип компании ООО, поле ОГРН обязательно и состоит из 13 цифр, а если ИП — из 15») благодаря удобному синтаксису superRefine() и глубокой экосистеме генераторов моков и форм.
Валидация уникальности субдомена или email при регистрации пользователя: форма выполняет мгновенные синхронные проверки формата, а при потере фокуса запускает серверный RPC с отменой предыдущего запроса через AbortController.
Сравним реальные технические характеристики трех библиотек при валидации полезной нагрузки пользователя (10 полей: строки, числа, вложенный массив):
Спроектируем одну и ту же модель профиля организации во всех трех валидаторах:
// ==========================================
// 1. РЕАЛИЗАЦИЯ НА ZOD
// ==========================================
import { z } from 'zod';
export const ZodOrgSchema = z.object({
name: z.string().min(2, 'Минимум 2 символа').max(50),
membersCount: z.number().int().positive('Должно быть положительным целым'),
taxId: z.string().regex(/^\d{10}$/, 'ИНН должен состоять из 10 цифр'),
isActive: z.boolean().default(true),
billingEmail: z.string().email('Некорректный email'),
});
export type ZodOrgInput = z.infer<typeof ZodOrgSchema>;
// ==========================================
// 2. РЕАЛИЗАЦИЯ НА VALIBOT (Модульный Tree-Shaking)
// ==========================================
import {
object,
string,
number,
boolean,
pipe,
minLength,
maxLength,
integer,
minValue,
regex,
email,
optional,
type InferOutput
} from 'valibot';
export const ValibotOrgSchema = object({
name: pipe(string(), minLength(2, 'Минимум 2 символа'), maxLength(50)),
membersCount: pipe(number(), integer(), minValue(1, 'Должно быть положительным целым')),
taxId: pipe(string(), regex(/^\d{10}$/, 'ИНН должен состоять из 10 цифр')),
isActive: optional(boolean(), true),
billingEmail: pipe(string(), email('Некорректный email')),
});
export type ValibotOrgInput = InferOutput<typeof ValibotOrgSchema>;
// ==========================================
// 3. РЕАЛИЗАЦИЯ НА TYPEBOX (JSON Schema + JIT)
// ==========================================
import { Type, type Static } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
export const TypeBoxOrgSchema = Type.Object({
name: Type.String({ minLength: 2, maxLength: 50 }),
membersCount: Type.Integer({ minimum: 1 }),
taxId: Type.String({ pattern: '^\\d{10}$' }),
isActive: Type.Optional(Type.Boolean({ default: true })),
billingEmail: Type.String({ format: 'email' }),
});
export type TypeBoxOrgInput = Static<typeof TypeBoxOrgSchema>;
// Компиляция схемы в высокоскоростной машинный код V8
export const CompiledOrgValidator = TypeCompiler.Compile(TypeBoxOrgSchema);Пакет @hookform/resolvers предоставляет готовые адаптеры для всех трех решений:
// Интеграция с React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { valibotResolver } from '@hookform/resolvers/valibot';
import { typeboxResolver } from '@hookform/resolvers/typebox';
// Для Valibot:
const valibotForm = useForm<ValibotOrgInput>({
resolver: valibotResolver(ValibotOrgSchema),
});
// Для TypeBox (с использованием скомпилированного валидатора):
const typeboxForm = useForm<TypeBoxOrgInput>({
resolver: typeboxResolver(TypeBoxOrgSchema),
});Одной из самых уязвимых зон валидации форм являются асинхронные проверки (проверка уникальности имени пользователя, существования организации, доступности доменного имени).
При быстром наборе текста пользователем запросы могут завершаться в произвольном порядке (Network Race Condition): запрос для текста user может прийти позже запроса для user123, затерев актуальное состояние ошибки.
Ниже приведена надежная реализация асинхронного валидатора с отменой предыдущих сетевых вызовов:
import { z } from 'zod';
// Глобальная переменная или реф для удержания активного AbortController
let checkSlugController: AbortController | null = null;
async function checkSlugAvailability(slug: string, signal: AbortSignal): Promise<boolean> {
const res = await fetch(`/api/check-slug?slug=${encodeURIComponent(slug)}`, {
signal,
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Ошибка сетевого шлюза');
const data = await res.json();
return data.isAvailable; // true если слаг свободен
}
export const AsyncTenantRegistrationSchema = z.object({
subdomain: z
.string()
.min(3, 'Длина слага должна быть не менее 3 символов')
.max(30, 'Длина не более 30 символов')
.regex(/^[a-z0-9-]+$/, 'Только строчные латинские буквы, цифры и дефис')
.refine(
async (slug) => {
// Отменяем предыдущий активный HTTP-запрос проверки
if (checkSlugController) {
checkSlugController.abort();
}
checkSlugController = new AbortController();
try {
return await checkSlugAvailability(slug, checkSlugController.signal);
} catch (err: unknown) {
// Если запрос был отменен контроллером, не считаем это ошибкой валидации
if (err instanceof DOMException && err.name === 'AbortError') {
return true;
}
return false;
}
},
{
message: 'Данный субдомен уже занят другой организацией',
}
),
companyName: z.string().min(2, 'Введите наименование компании'),
});TypeCompiler анализирует дерево JSON Schema, распознавая свойства, типы данных, ограничения длины и регулярные выражения.
Строится оптимизированный граф правил проверки без циклических зависимостей.
Компилятор формирует монолитную строку исходного кода функции проверки, заменяя полиморфные вызовы прямыми операторами сравнения.
Генерируется плоский код вида: function check(value) { return typeof value === 'object' && typeof value.name === 'string' && ... }
JavaScript-рантайм компилирует синтезированную строку в байткод V8, а TurboFan оптимизирует ее до ассемблерных инструкций CPU.
Устраняется необходимость в рекурсивных стеках вызовов и выделении промежуточных структур данных.
Скомпилированная функция вызывается для каждого входящего объекта формы. Проверка возвращает строго boolean без аллокаций памяти.
Скорость проверки достигает более 4 миллионов объектов в секунду на одно ядро процессора.
В каких окружениях использование TypeCompiler.Compile() приведет к фатальной ошибке выполнения, и как решить эту проблему?
Разработчики часто полагают, что именованный импорт в Zod оптимизирует бандл:
// ❌ ИЛЛЮЗИЯ ОПТИМИЗАЦИИ: Импорт не уменьшает размер бандла
import { object, string, number } from 'zod';Несмотря на синтаксис именованного импорта, внутри файла zod/index.js все примитивы привязаны к общему пространству классов и прототипов. Бандлер будет вынужден упаковать весь парсер целиком (~14 КБ). Если критичен каждый килобайт клиентского бандла, архитектурно правильным решением является переход на Valibot.
Использование методов принудительного приведения типов в Zod или Valibot может маскировать ошибки:
// ❌ ОПАСНО: Преобразование пустой строки в ноль
const schema = z.object({
age: z.coerce.number().min(18),
});Если в нативном HTML-инпуте пользователь очистил поле ввода, в обработчик попадет пустая строка "". Метод Number("") в JavaScript возвращает 0! Валидатор получит число 0 вместо undefined или null, выдав ошибку «Минимальный возраст 18», вместо ожидаемой ошибки «Обязательное поле». Для форм следует использовать явную трансформацию:
// ✅ ПРАВИЛЬНО: Строгая проверка пустой строки
const schema = z.object({
age: z.string().min(1, 'Обязательное поле').transform((val, ctx) => {
const parsed = Number(val);
if (isNaN(parsed)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Введите корректное число' });
return z.NEVER;
}
return parsed;
}).pipe(z.number().min(18, 'Минимум 18 лет')),
});Если асинхронная проверка схемы (например, проверка уникальности слага) не использует отмену предыдущего запроса через AbortController, медленный сетевой ответ на старое значение (например, на запрос с a) может прийти после быстрого ответа на новое значение (abc). В результате интерфейс отобразит ложное сообщение об ошибке для уже измененного поля.
Вызов TypeCompiler.Compile() в клиентском коде SPA-приложения с заголовком безопасности Content-Security-Policy: default-src 'self' вызовет фатальное исключение браузера EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script. Для клиентских бандлов с жестким CSP следует применять либо Valibot/Zod, либо Value.Check(Schema, data).