Back

Как типизировать ответы API в TypeScript

Как типизировать ответы API в TypeScript

Каждый frontend-разработчик сталкивался с этим: вы получаете данные из API, обращаетесь к свойству и получаете undefined во время выполнения — хотя TypeScript ни на что не жаловался. Проблема не в вашем коде. Дело в том, что система типов TypeScript работает только во время компиляции. Она понятия не имеет, что на самом деле возвращает ваш API.

В этой статье рассматриваются практические паттерны типизации ответов API в TypeScript — от определения базовых интерфейсов до валидации во время выполнения и генерации типов на основе контрактов.

Ключевые выводы

  • Система типов TypeScript работает только во время компиляции — она не может валидировать фактическую структуру данных, возвращаемых API во время выполнения.
  • Используйте интерфейсы или псевдонимы типов с размеченными объединениями для определения четких ожидаемых структур ответов.
  • Универсальная обёртка для fetch делает код в местах вызова чище и явно указывает ожидаемые типы.
  • Для недоверенных или внешних данных используйте библиотеки валидации во время выполнения, такие как Zod, Valibot или ArkType, чтобы устранить разрыв между типами времени компиляции и реальными ответами.
  • Когда существует спецификация OpenAPI, генерируйте типы из неё, чтобы поддерживать синхронизацию контрактов между frontend и backend.

Почему TypeScript не может защитить вас во время выполнения

Когда вы вызываете response.json(), TypeScript обычно рассматривает результат как unknown (или иногда any, в зависимости от типизации окружения). Это означает, что вы можете привести его к любому типу — и компилятор полностью вам доверится.

// ❌ TypeScript слепо доверяет этому приведению типа
const data = (await response.json()) as User
console.log(data.email) // Может быть undefined во время выполнения

Это ключевая граница, которую нужно понимать: типизированные ответы API в TypeScript дают вам безопасность во время компиляции, но они не валидируют фактический JSON, который возвращает ваш API. Если структура изменится, TypeScript об этом не узнает.

Определение типов для ожидаемых структур ответов

Первый шаг — придать структуру тому, что вы ожидаете. Используйте интерфейсы или псевдонимы типов для описания структуры ответа:

interface User {
  id: string
  name: string
  email: string
}

Для согласованных API универсальная обёртка чисто обрабатывает как успешные, так и ошибочные состояния:

type ApiResponse<T> =
  | { status: "ok"; data: T }
  | { status: "error"; message: string }

Этот паттерн размеченного объединения более прозрачен, чем единственный тип с множеством опциональных полей. Когда вы проверяете status, TypeScript автоматически сужает тип — дополнительные проверки не нужны.

Типизация ответов Fetch с помощью универсальной обёртки

Типизация ответов fetch в TypeScript становится чище с переиспользуемым помощником:

async function apiFetch<T>(url: string): Promise<T> {
  const response = await fetch(url)
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
  return response.json() as Promise<T>
}

// Использование
const user = await apiFetch<User>("/api/users/1")

Это делает места вызова чище, явно указывая ожидаемую структуру. Тот же паттерн работает с Axios или любым другим HTTP-клиентом.

Валидация во время выполнения: устранение разрыва

Утверждения типов вроде as User — это обещание компилятору, а не гарантия. Для недоверенных внешних данных — сторонних API, пользовательского контента или любого ответа, который вы не полностью контролируете — вам нужна валидация во время выполнения.

Zod — наиболее широко используемый вариант. Вы определяете схему, парсите ответ и получаете полностью типизированный результат:

import { z } from "zod"

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

type User = z.infer<typeof UserSchema> // Выведен из схемы

const data = await response.json()
const user = UserSchema.parse(data) // Выбрасывает исключение, если структура неверна

Ключевое преимущество: ваши типы TypeScript и валидация во время выполнения остаются синхронизированными, потому что они выводятся из одного источника истины.

Альтернативы, о которых стоит знать:

  • Valibot — меньший размер бандла, API похож на Zod
  • ArkType — высокая производительность, нативный синтаксис TypeScript

Используйте .parse(), когда хотите выбросить исключение при невалидных данных, или .safeParse(), когда хотите обработать ошибки корректно без исключений.

Типизация на основе контрактов с OpenAPI

Если у вашего API есть спецификация OpenAPI, вы можете автоматически генерировать типы TypeScript с помощью openapi-typescript:

npx openapi-typescript ./api-spec.yaml -o ./types/api.d.ts

Этот подход поддерживает ваши типы в синхронизации с документацией API. Он дополняет валидацию во время выполнения — сгенерированные типы обрабатывают уровень времени компиляции, в то время как Zod или Valibot обрабатывают уровень времени выполнения.

Выбор правильного подхода

СценарийРекомендуемый паттерн
Внутренний API, который вы контролируетеИнтерфейс + утверждение типа
Общий контракт с backendТипы, сгенерированные из OpenAPI
Сторонний или недоверенный APIВалидация схемы Zod/Valibot
Нужны оба уровня безопасностиСгенерированные типы + схема времени выполнения

Заключение

TypeScript даёт вам структуру и автодополнение, но он не может валидировать то, что приходит по сети. Для API, которым вы доверяете, достаточно хорошо определённого интерфейса и типизированной обёртки для fetch. Для внешних или непредсказуемых данных дополните ваши типы валидатором времени выполнения вроде Zod. Когда у вас есть спецификация API, генерируйте типы из неё. Эти подходы не конкурируют — они лучше всего работают вместе.

Часто задаваемые вопросы

Нет. Ключевое слово 'as' — это утверждение типа, а не проверка во время выполнения. Оно сообщает компилятору рассматривать значение как определённый тип, но ничего не делает для проверки фактических данных. Если структура ответа API отличается от того, что вы утверждали, вы получите ошибки времени выполнения, которые TypeScript не может отловить.

Используйте Zod или аналогичную библиотеку валидации во время выполнения всякий раз, когда вы потребляете данные из источника, который вы не полностью контролируете, например сторонних API или публичных эндпоинтов. Если вы владеете и frontend, и backend и имеете высокую уверенность в структуре ответа, простого интерфейса с утверждением типа часто достаточно.

Да, и эта комбинация рекомендуется для максимальной безопасности. Типы, сгенерированные из OpenAPI, дают вам автодополнение и проверку типов во время компиляции, в то время как схемы Zod валидируют фактические данные ответа во время выполнения. Вместе они покрывают оба уровня спектра типобезопасности.

Метод response.json() Fetch API по умолчанию возвращает Promise<any> в TypeScript. Это означает, что компилятор не будет применять никаких проверок типов к результату, если вы явно не утвердите или не валидируете его. Некоторые команды переопределяют это более строгими типизациями, которые возвращают unknown, заставляя выполнять явную валидацию.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay