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 }.
Быстрый контрольный список
- Захватывайте богатый, реальный образец и проверяйте его.
- Создание интерфейсов с JSON в TypeScript преобразователь.
- Переименовать типы на язык домена.
- Исправьте необязательные поля, буквальные союзы и типы идентификаторов.
- Замените смешанные массивы на дискриминируемые профсоюзы.
- Добавить проверку времени выполнения для внешних данных.
- Переключение на генерацию на основе схем при наличии схем OpenAPI или GraphQL.
Следование этим шагам занимает несколько минут на конечную точку и окупается во много раз в автозаполнительных, более безопасных рефакторах и ошибках, обнаруженных до того, как они достигнут пользователей.
Эта страница переведена с английского автоматически. Если вы заметили ошибку, сообщите нам.