Недавно я обнаружил, что далеко не все знают об одной простой вещи — и это меня удивило.
Многие думают примерно так: «я описал интерфейс, указал что GET /users возвращает User[], TypeScript подсвечивает все поля, автодополнение работает — значит всё под контролем». Звучит разумно. Но есть одна проблема.
TypeScript работает только во время компиляции. В рантайме его нет.
Когда реальный ответ прилетает с сервера — никакой проверки не происходит. TypeScript просто верит вашей аннотации и молчит. Что реально вернул сервер — string вместо number, переименованное поле, отсутствующий объект — он не знает и знать не будет.
Вы узнаете об этом позже. Обычно от пользователей.
TypeScript — это иллюзия безопасности на границе с внешним миром
Когда вы пишете:
interface User { id: number; name: string; email: string; } const getUser = async (): Promise<User> => { const res = await fetch('/api/user'); return res.json(); // TypeScript просто верит вам на слово };
TypeScript не проверяет, что реально вернул сервер. Вы говорите «здесь будет User» — он верит.
Когда это реально спасает
1. Бэкенд изменил структуру без предупреждения
Было user.full_name, стало user.fullName. Фронт продолжает читать full_name — получает undefined. Без валидации вы узнаете об этом от пользователей или из Sentry.
2. Поле пришло неожиданного типа
Бэкенд вернул "123" вместо 123. TypeScript у вас написано number, но в рантайме — строка. Где-то дальше sum + userId превращается в конкатенацию "1001" — и вы долго ищете баг.
3. Поле отсутствует при определённых условиях
API возвращает address только если пользователь его заполнил. Вы не знали об этом эдж-кейсе, написали user.address.city — и получаете краш у части аудитории.
4. Вы работаете со сторонним API
Документация устарела. Контракт не соблюдается. Поле то есть, то нет. Без валидации вы каждый раз гадаете.
Решение — рантайм-валидация
Для этого существует целый класс библиотек — schema validation (или runtime validators). Их задача: описать ожидаемую структуру данных и проверить реальные данные на соответствие этой структуре во время выполнения программы.
Самые популярные в экосистеме TypeScript:
Zod — наиболее распространённый выбор сегодня, хорошая интеграция с TypeScript, типы выводятся из схемы автоматически
Yup — часто встречается в проектах с Formik
Valibot — молодая альтернатива Zod с акцентом на tree-shaking и меньший бандл
Joi — пришёл из Node.js мира, активно используется на бэкенде
ArkType — новый игрок, ставит на максимальную производительность
Все они решают одну задачу: вы описываете схему — библиотека проверяет данные и говорит, соответствуют ли они ей.
Дальше покажу примеры на основе Zod — он сейчас де-факто стандарт на фронтенде и имеет самую большую экосистему интеграций.
Пример с Zod
Zod позволяет описать схему один раз — и получить сразу и тип, и рантайм-валидацию:
import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer<typeof UserSchema>; // тип выводится автоматически const getUser = async (): Promise<User> => { const res = await fetch('/api/user'); const data = await res.json(); return UserSchema.parse(data); // если структура не та — бросит ошибку };
Вывод
TypeScript — отличный инструмент, но он работает до границы с внешним миром. Всё что приходит снаружи — HTTP-ответы, localStorage, postMessage, данные из URL — не имеет гарантий типов в рантайме.
Рантайм-валидация закрывает эту дыру: вы явно описываете контракт, и любое отклонение от него становится видимым сразу — не через баг-репорт от пользователя.
Это особенно важно в больших командах, где фронт и бэкенд развиваются независимо, и любое изменение API может незаметно сломать клиент.
