Валидация во время выполнения vs compile time

В системах, построенных на JavaScript и TypeScript, проверка типов на этапе компиляции является первой линией защиты от ошибок. TypeScript анализирует код до выполнения и гарантирует соответствие структур данных заявленным типам.

Основные свойства compile-time проверки

  • выполняется до запуска программы;
  • опирается на систему типов TypeScript;
  • полностью исчезает в рантайме после компиляции;
  • не работает с внешними данными (API, пользовательский ввод, файлы).

Пример:

type User = {
  id: number;
  name: string;
};

const user: User = {
  id: 1,
  name: "Alex",
};

На этапе компиляции TypeScript гарантирует, что объект соответствует типу User. Однако этот механизм имеет фундаментальное ограничение: он не контролирует данные, приходящие извне программы.

const data = JSON.parse('{"id":"1","name":123}');

С точки зрения TypeScript после JSON.parse тип данных становится any. Проверка исчезает, и ошибка может проявиться только во время выполнения.


Runtime-валидация и необходимость проверки данных во время выполнения

Валидация во время выполнения (runtime validation) решает проблему недоверия к внешним данным. Любой вход в систему считается потенциально некорректным.

Источники таких данных:

  • HTTP-запросы;
  • ответы API;
  • localStorage / sessionStorage;
  • файлы конфигурации;
  • пользовательский ввод.

Без runtime-валидации система работает с предположениями, а не с гарантиями.


Zod как инструмент runtime-валидации

Zod представляет собой библиотеку, ориентированную на описание схем данных и их проверку во время выполнения с автоматическим выводом типов TypeScript.

Ключевая идея: одна схема = и валидация, и тип.


Разрыв между compile time и runtime

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

Пример проблемы

type User = {
  id: number;
  name: string;
};

const response = await fetch("/api/user");
const data: User = await response.json();

TypeScript считает data корректным User, но фактически сервер может вернуть:

{
  "id": "not-a-number",
  "name": true
}

Compile-time проверка здесь бесполезна, потому что:

  • типы не существуют в runtime;
  • fetch возвращает any-подобную структуру;
  • доверие к API не проверяется системой типов.

Runtime-валидация как защита от недостоверных данных

Runtime-валидация вводит дополнительный слой:

  1. данные приходят в систему;
  2. проверяются по схеме;
  3. только после этого используются.

Схемы Zod как мост между двумя мирами

Zod объединяет:

  • описание структуры данных;
  • проверку на этапе выполнения;
  • автоматическое выведение TypeScript-типа.

Базовый пример

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
});

type User = z.infer<typeof UserSchema>;

Здесь:

  • UserSchema работает в runtime;
  • User — статический TypeScript-тип;
  • оба синхронизированы.

Сравнение подходов

Compile-time (TypeScript)

  • предотвращает ошибки в коде;
  • работает только внутри проекта;
  • не видит внешние данные;
  • не существует в runtime.

Runtime (Zod)

  • проверяет реальные значения;
  • работает с API и JSON;
  • обеспечивает безопасность данных;
  • имеет накладные расходы выполнения.

Типичный сценарий без runtime-валидации

type Product = {
  id: number;
  price: number;
};

async function getProduct(): Promise<Product> {
  const res = await fetch("/product");
  return res.json();
}

Проблема: res.json() не гарантирует структуру Product.


Сценарий с Zod

import { z } from "zod";

const ProductSchema = z.object({
  id: z.number(),
  price: z.number(),
});

type Product = z.infer<typeof ProductSchema>;

async function getProduct(): Promise<Product> {
  const res = await fetch("/product");
  const json = await res.json();

  return ProductSchema.parse(json);
}

Теперь:

  • некорректные данные вызовут ошибку сразу;
  • тип Product соответствует реальной структуре;
  • исчезает разрыв между ожиданием и реальностью.

Строгая граница доверия к данным

Runtime-валидация вводит важный принцип:

любые данные вне текущего процесса считаются недостоверными до проверки

Это включает:

  • API ответы;
  • параметры URL;
  • формы;
  • WebSocket сообщения.

Safe parsing вместо доверенного присваивания

Одна из ключевых концепций Zod — парсинг вместо кастинга типов.

Ошибочный подход

const user = data as User;

Это не проверка, а обещание компилятору.

Корректный подход

const user = UserSchema.parse(data);

Здесь происходит реальная проверка структуры.


Разновидности результата проверки

Zod поддерживает разные режимы обработки данных:

Строгая проверка

UserSchema.parse(data);
  • выбрасывает исключение при ошибке;
  • используется в критичных участках.

Безопасная проверка

UserSchema.safeParse(data);

Возвращает:

{
  success: boolean;
  data?: T;
  error?: ZodError;
}

Влияние на архитектуру приложений

Использование runtime-валидации приводит к архитектурному разделению:

Граница входа (boundary layer)

  • API слой;
  • десериализация;
  • валидация через схемы.

Внутренний слой

  • работает только с валидированными данными;
  • может полагаться на TypeScript без дополнительных проверок.

Проблема дублирования типов и её решение

До появления схемных библиотек существовала проблема:

  • типы описывались отдельно;
  • валидация писалась вручную;
  • синхронизация нарушалась.

Zod решает это через:

const Schema = z.object({
  id: z.number(),
});

type SchemaType = z.infer<typeof Schema>;

Композиция схем и сложные структуры

Runtime-валидация становится особенно важной при сложных структурах:

const OrderSchema = z.object({
  id: z.string(),
  items: z.array(
    z.object({
      productId: z.number(),
      quantity: z.number().min(1),
    })
  ),
});

Здесь TypeScript сам по себе не способен гарантировать корректность входных данных без runtime-проверки.


Ошибки как часть модели данных

В Zod ошибки становятся структурированными:

  • путь до поля;
  • тип ошибки;
  • ожидаемое значение;
  • фактическое значение.

Это делает возможной предсказуемую обработку ошибок.


Фундаментальное различие моделей

Compile-time модель

  • описывает намерение разработчика;
  • работает на уровне кода;
  • не взаимодействует с реальностью.

Runtime модель

  • описывает фактические данные;
  • проверяет реальность;
  • защищает систему от внешнего мира.

Сочетание двух уровней как стандарт

Современные TypeScript-приложения строятся на двойной системе:

  • TypeScript — для разработки и статической проверки;
  • Zod — для проверки реальных данных.

Их совместное использование создаёт замкнутую цепочку:

данные → runtime-валидация → типизированное использование → compile-time безопасность внутри системы