Zod - валидация с TypeScript

16 июня 2026
Автор

Олег Марков

Zod — валидация с TypeScript в React

Введение

Одна из главных проблем при работе с внешними данными (API-ответы, пользовательский ввод, конфигурационные файлы) — TypeScript не может гарантировать корректность данных во время выполнения. Вы можете описать тип User, но если API вернёт объект без обязательных полей, TypeScript об этом не знает.

Zod решает эту проблему: это TypeScript-first библиотека для объявления и валидации схем, которая одновременно обеспечивает type-safety во время компиляции и валидацию данных во время выполнения. В отличие от других библиотек (например, Yup), Zod разработан с нуля с расчётом на TypeScript и имеет более строгий и предсказуемый API.

Zod скачивают более 9 миллионов раз в неделю — это один из самых популярных инструментов в TypeScript-экосистеме. Его используют в tRPC, Prisma, Next.js и многих других проектах.

Установка

npm install zod
# или
yarn add zod

Zod требует TypeScript >= 4.5 и strict: true в tsconfig.json. Отдельных @types пакетов не нужно — типы уже включены.

{
  "compilerOptions": {
    "strict": true
  }
}

Ключевое отличие от Yup: parse, не validate

Философия Zod основана на парсинге, а не просто валидации. Метод .parse() возвращает трансформированные данные (или выбрасывает ошибку), а .safeParse() возвращает результирующий объект без исключений.

import { z } from 'zod';

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

// .parse() — выбрасывает ZodError при ошибке
const user = UserSchema.parse(rawData); // user имеет тип { id: number; name: string; email: string }

// .safeParse() — возвращает { success: true, data } или { success: false, error }
const result = UserSchema.safeParse(rawData);
if (result.success) {
  console.log(result.data); // полная типизация
} else {
  console.error(result.error.issues);
}

Базовые типы

Примитивы

import { z } from 'zod';

// Строки
const stringSchema = z.string();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const minMaxSchema = z.string().min(3).max(50);
const regexSchema = z.string().regex(/^[a-z]+$/, 'Только строчные буквы');
const trimSchema = z.string().trim();

// Числа
const numberSchema = z.number();
const intSchema = z.number().int();
const positiveSchema = z.number().positive();
const rangeSchema = z.number().min(0).max(100);

// Булевы
const boolSchema = z.boolean();

// Даты
const dateSchema = z.date();
const dateMinSchema = z.date().min(new Date('2020-01-01'));

// BigInt
const bigIntSchema = z.bigint();

// Символы
const symbolSchema = z.symbol();

// undefined, null, void, any, unknown, never
const undefinedSchema = z.undefined();
const nullSchema = z.null();
const anySchema = z.any();
const unknownSchema = z.unknown();

Объекты

const PersonSchema = z.object({
  name: z.string(),
  age: z.number().int().positive(),
  email: z.string().email().optional(),
});

// Тип выводится автоматически
type Person = z.infer<typeof PersonSchema>;
// { name: string; age: number; email?: string }

// Вложенные объекты
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string().default('RU'),
});

const UserSchema = z.object({
  id: z.number(),
  person: PersonSchema,
  address: AddressSchema.optional(),
});

Массивы

const TagsSchema = z.array(z.string());
const NumbersSchema = z.array(z.number()).min(1).max(10);
const NonEmptyArray = z.array(z.string()).nonempty(); // Тип [string, ...string[]]

// Кортежи
const TupleSchema = z.tuple([z.string(), z.number(), z.boolean()]);
// [string, number, boolean]

Перечисления и объединения

// Enum
const RoleSchema = z.enum(['admin', 'user', 'moderator']);
type Role = z.infer<typeof RoleSchema>; // 'admin' | 'user' | 'moderator'

// Нативный TypeScript enum
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
}
const DirectionSchema = z.nativeEnum(Direction);

// Union (объединение)
const IdSchema = z.union([z.string(), z.number()]);
// Или краткая форма:
const IdSchema2 = z.string().or(z.number());

// Discriminated union (дискриминированное объединение)
const ResultSchema = z.discriminatedUnion('status', [
  z.object({ status: z.literal('success'), data: z.string() }),
  z.object({ status: z.literal('error'), message: z.string() }),
]);

// Intersection (пересечение)
const AdminSchema = z.object({ role: z.literal('admin') });
const BaseUserSchema = z.object({ name: z.string() });
const AdminUserSchema = BaseUserSchema.and(AdminSchema);

Автоматический вывод типов

Главная суперсила Zod — вывод TypeScript-типов из схем:

const ProductSchema = z.object({
  id: z.number(),
  name: z.string(),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()).optional(),
  metadata: z.record(z.string()).optional(),
});

// Zod выводит точный тип
type Product = z.infer<typeof ProductSchema>;
/*
{
  id: number;
  name: string;
  price: number;
  category: 'electronics' | 'clothing' | 'food';
  tags?: string[];
  metadata?: Record<string, string>;
}
*/

// Теперь Product — это живой тип, всегда синхронизированный со схемой
function processProduct(product: Product) {
  // TypeScript знает все поля и их типы
}

Трансформации

Zod позволяет трансформировать данные в процессе парсинга:

// .transform() — преобразование данных
const TrimmedString = z.string().transform((s) => s.trim());
const NumberFromString = z.string().transform((s) => parseInt(s, 10));

// .preprocess() — обработка до валидации
const NumberSchema = z.preprocess(
  (val) => (typeof val === 'string' ? parseInt(val, 10) : val),
  z.number()
);

// Цепочка: сначала валидация, потом трансформация
const FormDateSchema = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .transform((s) => new Date(s));

// Тип у результата будет Date, а не string
type FormDate = z.infer<typeof FormDateSchema>; // Date

Опциональные и nullable поля

// optional: значение или undefined
const schema1 = z.string().optional(); // string | undefined

// nullable: значение или null
const schema2 = z.string().nullable(); // string | null

// nullish: значение, null или undefined
const schema3 = z.string().nullish(); // string | null | undefined

// Значения по умолчанию
const schema4 = z.string().default('Аноним');
const schema5 = z.number().default(0);

// Функция как дефолтное значение
const schema6 = z.date().default(() => new Date());

Рефайнменты и кастомная валидация

// .refine() — кастомное правило
const PasswordSchema = z.string()
  .min(8, 'Минимум 8 символов')
  .refine(
    (val) => /[A-Z]/.test(val),
    { message: 'Должна быть хотя бы одна заглавная буква' }
  )
  .refine(
    (val) => /[0-9]/.test(val),
    { message: 'Должна быть хотя бы одна цифра' }
  );

// .superRefine() — расширенный контроль ошибок
const RegistrationSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Пароли не совпадают',
      path: ['confirmPassword'], // Привязываем ошибку к конкретному полю
    });
  }
});

// Асинхронный refine (например, проверка в базе данных)
const UniqueEmailSchema = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  },
  { message: 'Email уже используется' }
);

Схемы объектов: pick, omit, partial, required

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  password: z.string(),
  role: z.enum(['admin', 'user']),
});

// Выбрать только нужные поля
const PublicUserSchema = UserSchema.pick({ id: true, name: true, email: true });

// Исключить поля
const UserWithoutPassword = UserSchema.omit({ password: true });

// Сделать все поля необязательными (для PATCH запросов)
const UpdateUserSchema = UserSchema.partial();

// Сделать только некоторые поля необязательными
const PartialUserSchema = UserSchema.partial({ email: true, role: true });

// Сделать все поля обязательными
const RequiredUserSchema = UserSchema.required();

// Расширить схему
const AdminUserSchema = UserSchema.extend({
  permissions: z.array(z.string()),
  lastLogin: z.date().optional(),
});

// Объединить две схемы
const MergedSchema = UserSchema.merge(AdminUserSchema);

Интеграция с React Hook Form

Zod отлично работает с React Hook Form через @hookform/resolvers:

npm install @hookform/resolvers
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Определяем схему
const loginSchema = z.object({
  email: z.string().email('Некорректный email'),
  password: z.string().min(8, 'Минимум 8 символов'),
  rememberMe: z.boolean().default(false),
});

// Выводим тип
type LoginFormData = z.infer<typeof loginSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false,
    },
  });

  const onSubmit = async (data: LoginFormData) => {
    // data здесь полностью типизирован как LoginFormData
    await login(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('email')} type="email" placeholder="Email" />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <input {...register('password')} type="password" placeholder="Пароль" />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <label>
        <input {...register('rememberMe')} type="checkbox" />
        Запомнить меня
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Вход...' : 'Войти'}
      </button>
    </form>
  );
}

Валидация API-ответов

Одно из главных применений Zod — валидация данных от внешних API:

import { z } from 'zod';

const ApiUserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(), // ISO 8601 строка
  role: z.enum(['admin', 'user']).default('user'),
});

type ApiUser = z.infer<typeof ApiUserSchema>;

async function fetchUser(id: number): Promise<ApiUser> {
  const response = await fetch(`/api/users/${id}`);
  const rawData = await response.json();

  // Zod проверит данные и выбросит ошибку, если они некорректны
  return ApiUserSchema.parse(rawData);
}

// Или безопасная версия
async function fetchUserSafe(id: number) {
  const response = await fetch(`/api/users/${id}`);
  const rawData = await response.json();

  const result = ApiUserSchema.safeParse(rawData);
  if (!result.success) {
    console.error('Невалидный ответ API:', result.error.issues);
    return null;
  }
  return result.data; // ApiUser
}

Обработка ошибок

import { z, ZodError } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  age: z.number().min(0),
});

try {
  schema.parse({ name: '', age: -1 });
} catch (err) {
  if (err instanceof ZodError) {
    // Все ошибки в одном месте
    err.issues.forEach((issue) => {
      console.log(`Путь: ${issue.path.join('.')}`);
      console.log(`Код: ${issue.code}`);
      console.log(`Сообщение: ${issue.message}`);
    });

    // Преобразование в формат { поле: 'ошибка' }
    const fieldErrors = err.flatten().fieldErrors;
    // { name: ['Минимум 1 символ'], age: ['Число должно быть >= 0'] }

    // Или более детальная структура
    const formatted = err.format();
  }
}

Кастомные сообщения об ошибках

// Встроенные сообщения об ошибках можно переопределить
const schema = z.object({
  name: z.string({
    required_error: 'Имя обязательно',
    invalid_type_error: 'Имя должно быть строкой',
  }).min(1, 'Имя не может быть пустым'),

  age: z.number({
    required_error: 'Возраст обязателен',
    invalid_type_error: 'Возраст должен быть числом',
  }).min(0, 'Возраст не может быть отрицательным'),
});

Сравнение Zod и Yup

Характеристика Zod Yup
TypeScript TypeScript-first Добавлен позже
Вывод типов Автоматический через z.infer<> Через yup.InferType<>
API Цепочки методов Цепочки методов
Производительность Быстрее Немного медленнее
Bundle size ~14kb ~40kb
Трансформации Встроены через .transform() Ограничены
Интеграция tRPC, Prisma, Next.js Formik, React Hook Form
Зрелость Активно развивается Режим поддержки

Когда выбрать Zod:

  • Проект на TypeScript (особенно с strict: true)
  • Нужна валидация API-ответов
  • Используете tRPC или Prisma
  • Важен размер бандла

Когда выбрать Yup:

  • Существующий проект с Formik
  • Нужна более сложная асинхронная валидация
  • Проект на JavaScript без TypeScript

Заключение

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

  • TypeScript-first — типы выводятся автоматически, схема и тип всегда синхронизированы
  • Runtime-валидация — защита от некорректных данных из внешних источников
  • Безопасный парсинг.safeParse() без исключений
  • Богатый API — трансформации, рефайнменты, условная логика
  • Экосистема — нативная поддержка в популярных библиотеках

Начните с валидации форм через React Hook Form, а затем расширьте использование на валидацию API-ответов и переменных окружения — Zod справится со всем этим элегантно.

Стрелочка влевоZustand — управление состоянием в ReactYup - валидация схемСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

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

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

React и Redux Toolkit

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

TypeScript с нуля

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

Next.js - с нуля

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

Отправить комментарий