Utilo

JSON к интерфейсам TypeScript: Практическое руководство по набору ответов API

Узнайте, как превратить JSON в точные интерфейсы TypeScript, обрабатывать необязательные поля, нули, союзы и массивы и поддерживать синхронизацию типов с реальными API.

· 4 мин чтения

Каждый проект TypeScript, который разговаривает с API, в конечном итоге сталкивается с одним и тем же вопросом: как описать форму данных, которые поступают в виде нетипированного JSON? Оставляя ответы как any, мы упускаем главную пользу от TypeScript. Написание интерфейсов вручную для 200-строчной полезной нагрузки утомительно и подвержено ошибкам. В этом руководстве рассказывается о надежном рабочем процессе по созданию интерфейсов из JSON, их уточнению и сохранению честности с течением времени.

Почему типы для JSON имеют значение

Когда вы вызовете await res.json(), TypeScript даст вам any. С этого момента типографические ошибки, такие как user.adress.city, компилируются без жалоб и только в момент выполнения, часто в процессе производства и часто для подмножества пользователей, чьи данные случайно отличаются. Записанный ответ дает вам автозаполнение, безопасное рефакторинг и компиляцию ошибок во время переименования поля.

Типы также служат документацией. Новый член команды, читающий interface Order { id: string; items: OrderItem[]; total: number; couponCode?: string }, узнает больше за пять секунд, чем прокручивая образцы ответов.

Шаг 1: Начните с реальной, репрезентативной выборки

Сгенерированные типы столь же хороши, как и образцы, которые вы подаете. Перед конвертированием, собрать ответ, который использует как можно больше полей:

  • Выберите запись с заполненными дополнительными полями, например, заказ с купоном, адресом доставки и сообщением о подарке.
  • Включайте массивы с хотя бы одним элементом; пустой массив ничего не говорит генератору о его содержимом.
  • Если поле может быть null, попробуйте включить один пример, где он является нулевым, и один, где он не является.

Копируйте тело ответа с вкладки Сеть вашего браузера или из curl, вставьте его в форматировщик JSON и подтвердите, что он действителен. Затем переключите режим на → TypeScript.

Шаг 2: Составление первого черновика

Учитывая этот вход:

{
  "id": "ord_9f2",
  "createdAt": "2026-09-30T12:01:44Z",
  "customer": { "id": 42, "email": "ana@example.com" },
  "items": [
    { "sku": "TSHIRT-M", "qty": 2, "price": 19.5 },
    { "sku": "MUG", "qty": 1, "price": 12 }
  ],
  "coupon": null
}

генератор производит что-то вроде:

export interface Customer {
  id: number;
  email: string;
}

export interface Item {
  sku: string;
  qty: number;
  price: number;
}

export interface Root {
  id: string;
  createdAt: string;
  customer: Customer;
  items: Item[];
  coupon: null;
}

Это хорошая отправная точка, но она отражает только то, что содержал один образец. Следующие шаги превращают его в тип, который соответствует контракту API.

Шаг 3: Переименование и уточнение

Генераторы называют вложенные интерфейсы по ключам. Переименовывайте их в доменные термины: Root становится Order, Item становится OrderItem. Затем пересмотрите каждое поле с этими вопросами.

Может ли он отсутствовать? Если API упускает ключ в некоторых ответах, отметьте его как необязательное с ?. Факультативный (coupon?: Coupon) и нулевой (coupon: Coupon | null) отличаются: первый означает, что ключ может отсутствовать, второй означает, что он всегда присутствует, но может содержать null. Многие API делают и то, и другое, что вы выражаете как coupon?: Coupon | null.

** Был ли null единственным значением, которое было замечено?** Поле, введенное буквально как null, почти всегда означает, что выборка не была репрезентативной. Замените его на настоящий тип плюс | null.

Это действительно строка? Даты появляются как строки ISO 8601. Сохранить их как string в транспорте типа; преобразование в Date принадлежит в слое отображения. Вы можете задокументировать намерения с псевдонимом типа: type ISODateString = string.

Если status всегда является одним из нескольких значений, используйте союз букв: status: "pending" | "paid" | "shipped". Это дает исчерпывающую проверку switch.

** Идентификаторы безопасны как цифры?** Идентификаторы баз данных выше 2^53 теряют точность в JavaScript. Если ваш бэкэнд использует 64-разрядные целые числа, попросите его отправить идентификаторы в виде строк и напечатать их как string.

Уточненный результат:

export type ISODateString = string;

export interface Order {
  id: string;
  createdAt: ISODateString;
  status: "pending" | "paid" | "shipped" | "cancelled";
  customer: Customer;
  items: OrderItem[];
  coupon?: Coupon | null;
}

Шаг 4: Обработка массивов со смешанным содержанием

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

type FeedEvent =
  | { type: "comment"; id: string; text: string }
  | { type: "like"; id: string; userId: string }
  | { type: "follow"; id: string; followerId: string };

Теперь TypeScript сужает тип автоматически внутри if (event.type === "comment"). При преобразовании смешанного массива инструмент объединяет выведенные типы элементов; используйте этот выход в качестве подсказки для проектирования дискриминированного союза вручную.

Шаг 5: Подтверждение во время выполнения

Типы TypeScript исчезают во время компиляции. Если бэкэнд изменится, ваш код все равно получит новую форму, и ваши типы будут молча лежать. Для данных, которые пересекают границу доверия, добавить проверку времени выполнения с помощью библиотеки схем, такой как Zod, Valibot или ArkType и получить статический тип из схемы:

import { z } from "zod";

const OrderSchema = z.object({
  id: z.string(),
  createdAt: z.string().datetime(),
  items: z.array(z.object({ sku: z.string(), qty: z.number().int(), price: z.number() })),
  coupon: z.object({ code: z.string() }).nullable().optional(),
});

export type Order = z.infer<typeof OrderSchema>;
const order = OrderSchema.parse(await res.json());

Вы все еще можете использовать генератор для загрузки: генерировать интерфейс, а затем перевести его поле за полем в схему.

Шаг 6: предпочтите источник истины, если он существует

Если ваш API публикует OpenAPI (Swagger) или схему GraphQL, генерируйте типы из этого вместо образцов. Такие инструменты, как openapi-typescript и GraphQL Code Generator, производят точные типы, включая опциональность и enums, и они могут работать в CI, поэтому типы обновляются при изменении схемы. Образец на основе генерации светит для сторонних API без схем, быстрых прототипов, webhooks и устаревших услуг.

Интерфейс или тип?

Для форм объектов interface и type почти взаимозаменяемы. Интерфейсы могут быть расширены и объединены, а сообщения об ошибках ссылаются на них по имени, что помогает с большими объектами. Алисы типов требуются для союзов, тупелей и отображаемых типов. Общим соглашением являются интерфейсы для форм объектов и псевдонимы типов для всего остального.

Частые ловушки

  • Введите все как необязательное. Он чувствует себя безопасно, но заставляет нулевые проверки везде и скрывает реальные нарушения контракта. Сделайте поля необязательными только тогда, когда API фактически их исключает.
  • Доверие к одной выборке. Проверьте документацию или несколько ответов перед окончательным определением опциона.
  • Смешивание типов транспорта и домена. Сохраняйте сырой OrderDTO, который отражает JSON, и отображайте его в домене Order с реальными объектами Date и вычисленными полями.
  • ** Забудьте об обертывающих страницах.** Многие API обертывают списки в { data: [...], nextCursor: "..." }. Моделируйте это один раз как общее название: interface Page<T> { data: T[]; nextCursor: string | null }.

Быстрый контрольный список

  1. Захватывайте богатый, реальный образец и проверяйте его.
  2. Создание интерфейсов с JSON в TypeScript преобразователь.
  3. Переименовать типы на язык домена.
  4. Исправьте необязательные поля, буквальные союзы и типы идентификаторов.
  5. Замените смешанные массивы на дискриминируемые профсоюзы.
  6. Добавить проверку времени выполнения для внешних данных.
  7. Переключение на генерацию на основе схем при наличии схем OpenAPI или GraphQL.

Следование этим шагам занимает несколько минут на конечную точку и окупается во много раз в автозаполнительных, более безопасных рефакторах и ошибках, обнаруженных до того, как они достигнут пользователей.

Эта страница переведена с английского автоматически. Если вы заметили ошибку, сообщите нам.

Похожие руководства