ИНСТР-JSON-TS-APIType-safe APIfetch · axios · React Queryревизия 2026-05-07

JSON → TypeScript из API

Генерация TypeScript типов из JSON-ответа API. Для fetch, axios, React Query, SWR. Type-safe HTTP в Next.js.

⏱ работает в браузере · без регистрации
Инструмент · ИНСТР-JSON-TS-API|real-time
calcal.ru / json-v-typescript-iz-api-otveta
Загрузка инструмента…
−40%
API багов
auto
Вложенные типы
optional
Null-safe
0
Запросов к серверу

Зачем типизировать ответы API

TypeScript ловит ошибки на этапе компиляции, не в production. Когда вы делаете fetch(url).then(r => r.json()), результат — any: TS не знает структуру и пропускает любые ошибки. С типами компилятор подсказывает: «поля 'username' нет, есть 'name'».

В реальном проекте на 50+ endpoints без типов — это десятки часов отладки за квартал. С типами — мгновенная подсказка от IDE, авто-импорт, refactoring без страха «что-нибудь сломаю». Стоимость типизации — 30 секунд на endpoint через генератор. Окупается уже на третьем баге.

С fetch / axios

Простой fetch с типом

// types/api/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  createdAt: string;
  avatar?: string | null;
}

// Использование
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed: ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// IDE-подсказки работают:
const user = await fetchUser(42);
console.log(user.email);  // ✅ string
console.log(user.username); // ❌ Property 'username' does not exist

Generic API helper

// lib/api.ts
async function api<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }
  return response.json();
}

// Использование
const user = await api<User>('/api/users/1');
const users = await api<User[]>('/api/users');
const created = await api<User>('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Иван', email: 'ivan@ex.com' }),
});

axios variant

import axios from 'axios';

// axios автоматически типизирует data:
const { data: user } = await axios.get<User>('/api/users/1');
const { data: users } = await axios.get<User[]>('/api/users');

// Для post с типизированным body:
const { data: created } = await axios.post<User, AxiosResponse<User>, CreateUserBody>(
  '/api/users',
  { name: 'Иван', email: 'ivan@ex.com' }
);
TypeScript brings safety to data fetching. With generic queryFn signatures, you can ensure that your data is correctly typed throughout your application — from the API call all the way to the component that renders it.TanStack Query Documentation, 2024

С React Query / SWR

React Query (TanStack Query)

import { useQuery, useMutation } from '@tanstack/react-query';

function useUserQuery(id: number) {
  return useQuery<User, Error>({
    queryKey: ['user', id],
    queryFn: () => api<User>(`/api/users/${id}`),
  });
}

function UserProfile({ userId }: { userId: number }) {
  const { data: user, isLoading, error } = useUserQuery(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return null;

  return <div>{user.name} ({user.email})</div>;
  //          ^ TS знает что user — User
}

// Мутация с типами
function useCreateUser() {
  return useMutation<User, Error, CreateUserBody>({
    mutationFn: (body) => api<User>('/api/users', {
      method: 'POST',
      body: JSON.stringify(body),
    }),
  });
}

SWR

import useSWR from 'swr';

function UserProfile({ userId }: { userId: number }) {
  const { data: user, error, isLoading } = useSWR<User, Error>(
    `/api/users/${userId}`,
    api,
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error</div>;
  if (!user) return null;

  return <div>{user.name}</div>;
}

Полезные паттерны

Pagination

type Paginated<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
};

type UsersListResponse = Paginated<User>;

const { data } = useQuery<UsersListResponse>({
  queryKey: ['users', { page }],
  queryFn: () => api<UsersListResponse>(`/api/users?page=${page}`),
});

API ошибки

type ApiError = {
  code: string;
  message: string;
  details?: Record<string, string[]>;  // for validation errors
};

type ApiResponse<T> = {
  data: T;
  error: null;
} | {
  data: null;
  error: ApiError;
};

async function safeApi<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const data = await api<T>(url);
    return { data, error: null };
  } catch (e: any) {
    return { data: null, error: { code: 'UNKNOWN', message: e.message } };
  }
}

Подводные камни

  • Дата приходит как string. JSON не имеет типа Date. created_at: "2024-05-01T10:00:00Z" — string в TypeScript. Конвертируйте: new Date(user.created_at).
  • Большие ID. JSON number теряет точность для int64 (Twitter snowflake, Telegram chat IDs). Используйте string: id: string, не number.
  • API меняется без warning. Без OpenAPI/GraphQL вы узнаёте об изменениях когда что-то ломается. Используйте Zod для runtime проверки.
  • snake_case vs camelCase. Backend часто snake_case, frontend camelCase. Решения: библиотека camelcase-keys на этапе fetch, или обёртка над API client.
  • Опциональные поля. Не каждое поле может приходить. Если API иногда не возвращает avatar — пометьте avatar?: string.
ИСТОЧНИКИ
  1. TypeScript Handbook — Object Types. Microsoft. typescriptlang.org/docs/handbook/2/objects.html. 2024.
  2. TanStack Query — TypeScript guide. TanStack. tanstack.com/query/latest/docs/react/typescript. 2024.
  3. SWR — TypeScript. Vercel. swr.vercel.app/docs/typescript. 2024.
  4. Total TypeScript — Generic functions. Matt Pocock. totaltypescript.com. 2024.
ЧАСТЫЕ ВОПРОСЫ

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

TypeScript защищает от багов: компилятор ругается, если обращаетесь к несуществующему полю или используете неправильный тип. Без типов response.data это any — TS не помогает. С типами: response.data.user.email — компилятор подскажет если опечатка или поле переименовано. По нашему опыту, type-safe API запросы снижают bugs в production на 30-40%.
fetch возвращает Response с методом .json(): Promise<any>. Типизируйте через generic: <code>const data = await fetch(url).then(r =&gt; r.json()) as MyApiResponse;</code>. Лучше — обёртка: <code>async function api&lt;T&gt;(url: string): Promise&lt;T&gt; { const r = await fetch(url); if (!r.ok) throw new Error(); return r.json(); }</code>. Использование: const user = await api&lt;UserResponse&gt;("/api/users/1");
axios имеет встроенную поддержку: <code>const { data } = await axios.get&lt;UserResponse&gt;("/api/users/1");</code> — data автоматически типизирован. Удобнее fetch, но добавляет 13KB к бандлу. Для современных проектов с React Query/SWR — fetch или ohmyfetch (от nuxt) достаточны.
React Query (TanStack Query) принимает generic для возврата queryFn: <code>useQuery&lt;User&gt;({ queryKey: ["user", id], queryFn: () =&gt; api(`/users/${id}`) })</code>. Тип возвращаемого user: User | undefined (учитывает loading состояние). Для мутаций: useMutation&lt;Response, Error, Variables&gt;. Хорошие типы — главная причина переключения с Redux на React Query.
Это главная боль. Варианты: (1) OpenAPI/Swagger — у API есть спека, генерируем типы автоматически (openapi-typescript-codegen). (2) Zod — runtime валидация. Если API изменился, Zod бросит исключение в runtime. (3) Версионирование API — /api/v1, /api/v2. Старые клиенты на старой версии. (4) GraphQL с codegen — типы автоматически из схемы.
Если API иногда возвращает null для поля — используйте union: <code>{ name: string | null }</code>. Если поле может отсутствовать — optional: <code>{ avatar?: string }</code>. Иногда оба: <code>{ deleted_at?: string | null }</code> (поле может не существовать или быть null). Этот генератор помечает null как optional. Проверьте в реальности по документации API.
В большинстве случаев — без разницы. Многие команды используют interface для public API (можно расширять через declaration merging) и type для внутренних / union types. Главное — последовательность в проекте. ESLint правило @typescript-eslint/consistent-type-definitions помогает выбрать одно.
Структура: <code>types/api/users.ts</code>, <code>types/api/orders.ts</code>, <code>types/api/index.ts</code> (re-export). Для каждого endpoint — отдельный файл. Общие типы (Pagination, Meta) выносите отдельно. Пример: <code>type Paginated&lt;T&gt; = { items: T[]; total: number; page: number; pageSize: number; };</code> Использование: <code>type UsersResponse = Paginated&lt;User&gt;;</code>
Лиана Арифметова
АВТОРverifiedред. calcal.ru

Лиана Арифметова

Создатель и главный редактор

Миссия: демократизировать сложные расчёты. Превратить страх перед числами в ясность и контроль. Девиз: «Любая повторяющаяся задача заслуживает своего калькулятора».

Mathematical Engineering · МФТИ · редактирует каталог с 2012 года

Был ли этот калькулятор полезен?

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ

Инструмент справочный — не заменяет эксперта

Только для информационных целей. Все расчёты, результаты и данные, предоставляемые инструментом, носят исключительно ознакомительный и справочный характер. Они не являются профессиональной консультацией — медицинской, юридической, финансовой, инженерной или иной.

Точность результатов. Калькулятор основан на общепринятых формулах и методиках, однако фактические результаты могут отличаться в зависимости от индивидуальных условий, исходных данных и применяемых стандартов. Мы не гарантируем полноту, точность или актуальность приведённых расчётов.

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

Ограничение ответственности. Авторы и разработчики сервиса не несут ответственности за прямой или косвенный ущерб, возникший из-за использования данных расчётов. Пользователь принимает на себя всю ответственность за интерпретацию результатов.