Что такое Zod и зачем нужна runtime-валидация в TypeScript-проекте?

MiddleTypeScript · Frontend·Обновлено 9 августа 2026
Коротко
Zod — библиотека для декларативного описания схем данных и их валидации во время выполнения. TypeScript проверяет типы только на этапе компиляции, поэтому без runtime-валидации данные извне (API, формы, env) могут нарушить типовые контракты и привести к неожиданным ошибкам в продакшене.

Проблема: TypeScript не защищает на этапе выполнения

TypeScript — статическая система типов. Это значит, что все проверки происходят во время компиляции и полностью исчезают из скомпилированного JavaScript-кода. Если сервер вернул объект с неожиданной структурой, TypeScript об этом не узнает — ваш код просто получит undefined там, где ожидал строку, и упадёт или отработает некорректно.

Основные источники «опасных» данных:

  • Ответы внешних и внутренних API
  • Данные из форм пользователя
  • Переменные окружения (process.env)
  • Данные из localStorage или sessionStorage
  • Параметры URL и query-строки

Что такое Zod

Zod — TypeScript-first библиотека для объявления и валидации схем данных. Её ключевые принципы:

  1. Схема = источник истины. Вы описываете структуру один раз, а Zod автоматически выводит TypeScript-тип через z.infer<typeof schema>.
  2. Валидация в рантайме. Метод parse или safeParse проверяет реальные данные и бросает подробную ошибку при несоответствии.
  3. Нулевые зависимости, маленький бандл. Zod не тянет лишних пакетов.

Базовое использование

import { z } from 'zod';

// Объявляем схему
const UserSchema = z.object({
  id: z.number().int().positive(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.string().datetime().optional(),
});

// Выводим TypeScript-тип автоматически — не дублируем код
type User = z.infer<typeof UserSchema>;

// parse — бросает ZodError при ошибке
const user = UserSchema.parse(rawDataFromApi);

// safeParse — возвращает { success, data } | { success: false, error }
const result = UserSchema.safeParse(rawDataFromApi);
if (!result.success) {
  console.error(result.error.flatten());
} else {
  console.log(result.data.email); // тип User гарантирован
}

Интеграция с fetch-запросами

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json = await response.json(); // тип unknown

  // Без Zod пришлось бы доверять API или писать ручные проверки
  return UserSchema.parse(json); // гарантируем корректность
}

Валидация переменных окружения

const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000), // coerce конвертирует строку в число
  NODE_ENV: z.enum(['development', 'production', 'test']),
});

// Падаем при старте приложения, а не в рантайме через час работы
export const env = EnvSchema.parse(process.env);

Преобразования и трансформации

Zod умеет не только валидировать, но и преобразовывать данные:

const DateSchema = z.string().transform((val) => new Date(val));
// parse вернёт Date, а не string

Сравнение с альтернативами

  • Yup — похожий API, но хуже интегрируется с TypeScript (менее точные выводимые типы).
  • io-ts — функциональный стиль, крутой вывод типов, но сложнее для освоения.
  • class-validator — декораторы на классах, требует reflect-metadata, не работает с plain объектами.
  • Valibot — более легковесная альтернатива Zod с модульным API.

Zod стал стандартом de facto благодаря простому API и первоклассной поддержке TypeScript.

Что хочет услышать интервьюер

Понимание разницы между compile-time и runtime: TypeScript-типы стираются после компиляции и не защищают от реальных данных

Знание конкретных сценариев, где нужна runtime-валидация: ответы API, пользовательский ввод, env-переменные

Умение показать базовый паттерн: объявить схему, вывести тип через z.infer, использовать parse/safeParse

Понимание преимущества единого источника истины: схема Zod одновременно является и TypeScript-типом

Осведомлённость об альтернативах (Yup, io-ts, Valibot) и умение объяснить, почему выбрали Zod

Пример: Базовая схема и вывод типа

import { z } from 'zod';

const ProductSchema = z.object({
  id: z.number().int().positive(),
  name: z.string().min(1).max(200),
  price: z.number().nonnegative(),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()).default([]),
});

// Тип выводится автоматически — не нужно объявлять отдельно
type Product = z.infer<typeof ProductSchema>;

// safeParse — безопасный вариант без исключений
function parseProduct(raw: unknown): Product | null {
  const result = ProductSchema.safeParse(raw);
  if (!result.success) {
    console.error('Невалидный продукт:', result.error.flatten());
    return null;
  }
  return result.data;
}

Пример: Валидация ответа API

import { z } from 'zod';

const ApiResponseSchema = z.object({
  data: z.array(
    z.object({
      id: z.number(),
      title: z.string(),
      completed: z.boolean(),
    })
  ),
  total: z.number(),
  page: z.number(),
});

type ApiResponse = z.infer<typeof ApiResponseSchema>;

async function fetchTodos(): Promise<ApiResponse> {
  const res = await fetch('/api/todos');
  // response.json() возвращает unknown — небезопасно без валидации
  const json: unknown = await res.json();

  // parse бросит ZodError с понятным сообщением, если структура не та
  return ApiResponseSchema.parse(json);
}

Пример: Валидация переменных окружения при старте

import { z } from 'zod';

const EnvSchema = z.object({
  DATABASE_URL: z.string().url('DATABASE_URL должен быть валидным URL'),
  // coerce автоматически преобразует строку '3000' в число 3000
  PORT: z.coerce.number().int().positive().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET слишком короткий'),
});

// Если переменные отсутствуют или невалидны — приложение упадёт сразу при старте
export const env = EnvSchema.parse(process.env);

// Использование с полной типизацией
console.log(env.PORT); // тип: number, не string | undefined

Типичные ошибки

Считать, что TypeScript-типы защищают данные в рантайме — путаница между статической и динамической типизацией

Использовать parse вместо safeParse там, где ошибку нужно обработать gracefully, а не бросить исключение

Дублировать TypeScript-интерфейс и Zod-схему отдельно вместо использования z.infer для вывода типа

Валидировать данные только на фронтенде, забывая про необходимость валидации на бэкенде (или наоборот)

Не использовать z.coerce для автоматического приведения типов (например, строки из query-параметров к числу)

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее