Недавно я обнаружил, что далеко не все знают об одной простой вещи — и это меня удивило.

Многие думают примерно так: «я описал интерфейс, указал что 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 может незаметно сломать клиент.