Что такое Zod и зачем нужна runtime-валидация в TypeScript-проекте?
Проблема: TypeScript не защищает на этапе выполнения
TypeScript — статическая система типов. Это значит, что все проверки происходят во время компиляции и полностью исчезают из скомпилированного JavaScript-кода. Если сервер вернул объект с неожиданной структурой, TypeScript об этом не узнает — ваш код просто получит undefined там, где ожидал строку, и упадёт или отработает некорректно.
Основные источники «опасных» данных:
- Ответы внешних и внутренних API
- Данные из форм пользователя
- Переменные окружения (
process.env) - Данные из
localStorageилиsessionStorage - Параметры URL и query-строки
Что такое Zod
Zod — TypeScript-first библиотека для объявления и валидации схем данных. Её ключевые принципы:
- Схема = источник истины. Вы описываете структуру один раз, а Zod автоматически выводит TypeScript-тип через
z.infer<typeof schema>. - Валидация в рантайме. Метод
parseилиsafeParseпроверяет реальные данные и бросает подробную ошибку при несоответствии. - Нулевые зависимости, маленький бандл. 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-параметров к числу)


