JSON в TypeScript Автоматическое создание интерфейсов из ответов API

Обновлено

Ручное написание интерфейсов TypeScript для каждого ответа API является трудоемким и подверженным ошибкам. Узнайте, как автоматически генерировать точные интерфейсы на основе реальных данных JSON, а затем добавьте проверку на выполнение в режиме выполнения с помощью Zod — поскольку типы исчезают на уровне выполнения и `any` не является решением.

JSON в TypeScript: Автоматическое создание интерфейсов из ответов API 1
Реклама · УДАЛИТЬ?

Вы только что получили данные из внешнего API. Ответ — это плотный блок JSON — вложенные объекты, массивы, поля с возможным отсутствием — и теперь вам нужно определить типы в TypeScript. Поэтому вы открываете новый файл и начинаете вводить текст interface User { ... }, и через 20 минут у вас получается что-то, что, вероятно, соответствует реальным данным. Вероятно.

Существует лучший способ. Инструменты, которые конвертируют JSON в интерфейсы TypeScript, сокращают эту 20-минутную задачу до нескольких секунд. В этой статье описаны типы, которые генерируются, как обрабатывать сложные случаи (отсутствующие значения, объединения, глубокая вложенность), и почему вы должны использовать генерированные типы вместе с схемами Zod для обнаружения несоответствий формы на уровне выполнения — а не только на уровне компиляции.

Почему интерфейсы TypeScript для ответов API важны

Преимущество TypeScript — обнаружение ошибок до выполнения кода. Без типизированных ответов API вы летите в темноту: обращаетесь к свойствам, которые могут не существовать, рассматриваете необязательные значения как обязательные или тихо преобразуете строку "null" в что-то неожиданное в дальнейшем.

Рассмотрим распространённую ситуацию:

const user = await fetchUser(id);
console.log(user.address.city); // TypeError at runtime if address is null

Если бы вы правильно типизировали ответ — с address: Address | null — TypeScript сразу бы выявила эту ошибку. Компилятор является вашей первой линией защиты, но только если вы предоставите ему что-то на что он сможет работать.

Ручное написание интерфейсов для каждого API является трудоёмким и подверженным ошибкам. Вы неверно читаете схему, пропускаете необязательное поле или копируете устаревшую версию. Генерация интерфейсов напрямую из реальных данных JSON устраняет эту человеческую ошибку.

Что даёт конвертация JSON в TypeScript

Рассмотрим простой ответ API:

{
  "id": 42,
  "username": "jsmith",
  "email": "j@example.com",
  "createdAt": "2024-01-15T10:30:00Z",
  "role": "admin",
  "profile": {
    "bio": "Developer",
    "avatar": null
  }
}

Вставьте это в Генератор интерфейсов TypeScript из JSON и вы получите:

interface Profile {
  bio: string;
  avatar: null;
}

interface RootObject {
  id: number;
  username: string;
  email: string;
  createdAt: string;
  role: string;
  profile: Profile;
}

Несколько моментов, на которые стоит обратить внимание:

  • Вложенные объекты превращаются в собственные интерфейсыProfile извлекается автоматически, а не встраивается.
  • Даты типизируются как string — JSON не имеет типа даты, поэтому строки в формате ISO остаются строками. Вам нужно будет их самостоятельно парсить.
  • avatar: null типизируется как литерал null — что является точным, но неполным. Более подробно об этом ниже.

Обработка сложных случаев

Поля с возможным отсутствием

при наличии в вашем образце JSON, генератор типизирует их как null . Но на практике это поле, вероятно, меняется между реальным значением и null в зависимости от данных. Вам нужно вручную скорректировать эти типы: nullТо же самое касается необязательных полей, которые в вашем образце оказываются заполненными — добавьте

// Generated
avatar: null;

// What you actually want
avatar: string | null;

к любому свойству, которое может отсутствовать в некоторых ответах. ? Массивы объектов обрабатываются чисто. При наличии:

Массивы

Генератор создаёт:

{
  "posts": [
    { "id": 1, "title": "Hello", "published": true },
    { "id": 2, "title": "World", "published": false }
  ]
}

Если в ваших образцах JSON показано, что поле содержит разные типы в разных записях — например, поле

interface Post {
  id: number;
  title: string;
  published: boolean;
}

interface RootObject {
  posts: Post[];
}

Объединяющие типы

которое может быть числом или строкой — вы должны представить это как объединение. Генерированные типы не обнаружат это на основе одного образца, поэтому стоит проверить документацию API: value Глубокая вложенность объектов

value: string | number;

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

Разрыв между типами на уровне компиляции и реальностью на уровне выполнения

Вот то, что часто пропускают новички в TypeScript:

типы исчезают на уровне выполнения. TypeScript компилируется в JavaScript, и JavaScript не имеет понятия об интерфейсах. Если ваш API возвращает форму, не соответствующую объявленному типу, TypeScript не узнает — и не предупредит вас. Это делает распространённый паттерн приведения ответов API действительно опасным:

Это приведение говорит TypeScript «верю, что это

const data = await response.json() as User; // No validation, just trust

» — но TypeScript не может проверить это. Если API изменяет свою форму или возвращает объект ошибки вместо ответа, ваш код ломается на уровне выполнения, и компилятор никогда не предупредил вас. UserРешением является проверка на уровне выполнения.

Zod для проверки на уровне выполнения

Zod

— это библиотека валидации схем, ориентированная на TypeScript. Вы определяете схему один раз, используете её для парсинга входных данных и получаете полностью типизированное значение — или подробную ошибку, если форма не соответствует. Без приведения, без предположений. Используя тот же образец JSON, который был ранее, Zod создаёт:

Обратите внимание на последнюю строку: Генератор JSON в Zod Schema получает тип TypeScript напрямую из схемы. Вы получаете как безопасность на уровне компиляции, так и проверку на уровне выполнения из одного источника правды.

import { z } from "zod";

const ProfileSchema = z.object({
  bio: z.string(),
  avatar: z.null(),
});

const RootObjectSchema = z.object({
  id: z.number(),
  username: z.string(),
  email: z.string(),
  createdAt: z.string(),
  role: z.string(),
  profile: ProfileSchema,
});

type RootObject = z.infer<typeof RootObjectSchema>;

Использование в точке получения данных выглядит так: z.infer Исправьте сгенерированную схему, чтобы обработать случаи с отсутствующими полями, которые генератор не может определить на основе одного образца:

Интерфейс против типа: какой вы должны выбрать?

const rawData = await response.json();
const user = RootObjectSchema.parse(rawData); // throws if shape is wrong

// Or use safeParse to avoid throwing:
const result = RootObjectSchema.safeParse(rawData);
if (!result.success) {
  console.error(result.error.issues);
} else {
  console.log(result.data.username); // fully typed
}

Алиасы могут представлять формы объектов в TypeScript, и для большинства типизации ответов API они взаимозаменяемы. Практические различия:

avatar: z.string().nullable(), // was z.null()
bio: z.string().optional(),    // if the field might be absent

Интерфейсы могут расширяться и объединяться

Оба interface и type — полезно, если вы хотите расширять базовый тип по файлам. Объединение объявлений позволяет добавлять поля к интерфейсу, определённому в другом месте.

  • Алиасы типов более гибки — они могут представлять объединения, пересечения, кортежи и отображённые типы, что интерфейсы не могут сделать.
  • Сообщения об ошибках обычно более чёткие с интерфейсами — TypeScript расширяет алиасы в сообщениях об ошибках, что может сделать глубоко вложенные ошибки трудными для чтения.
  • Для форм ответов API, любой из них подойдёт. Выберите если вы ожидаете расширение типа; используйте

если вам нужно использовать семантику объединения или пересечения. Важнее всего поддерживать единообразие в рамках кодовой базы, чем выбирать один из двух. interface Практический рабочий процесс type Вот рабочий процесс, который занимает минуты, а не целый день:

Соберите реальный ответ.

Используйте вкладку сети браузера, Postman или curl, чтобы получить реальный ответ API. Чем полнее образец, тем лучше будут сгенерированные типы.

  1. Сгенерируйте интерфейс TypeScript. Вставьте JSON в
  2. . Скопируйте результат в ваш проект. Сгенерируйте схему Zod. Генератор интерфейсов TypeScript из JSONВставьте тот же JSON в
  3. . Скопируйте это в ваш проект. Проверьте на наличие полей с отсутствием и необязательными значениями. Генератор JSON в Zod SchemaПроверьте сгенерированный вывод на поля, типизированные как литерал
  4. или поля, которые могут отсутствовать. Обновите их до Проверка на уровне получения данных. null Замените любые string | null, .nullable(), или .optional() по мере необходимости.
  5. на . Теперь тип гарантированно соответствует, а не просто предполагается. as YourType Это полный цикл — от сырого JSON до безопасности на уровне компиляции и гарантий на уровне выполнения за несколько минут. YourSchema.parse() или safeParse()JSON в TypeScript: Автоматическая генерация интерфейсов из ответов API 2

JSON в TypeScript: Автоматическая генерация интерфейсов из ответов API 1

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

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

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?