JSON в TypeScript

Вставьте JSON и получите чистые TypeScript-интерфейсы: вложенные объекты, типизированные массивы, дедупликация форм и своё имя корня. Работает в браузере.

Всё работает в вашем браузере — данные не отправляются на сервер.

Зачем генерировать типы из JSON

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

Что умеет

  • Интерфейсы для каждого вложенного объекта с корректными именами полей — с кавычками там, где нужно.
  • Типизированные массивы, включая объединения, когда массив смешивает разные формы.
  • Дедупликация форм и своё имя корневого интерфейса.

Как пользоваться

  1. Вставьте JSON — ответ API, конфиг, любой валидный.
  2. При желании поменяйте имя корневого интерфейса.
  3. Скопируйте готовый TypeScript в свой проект.

Как это работает

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

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

Обычная история разработки: фронтенд получил от бэкенда ответ, открыл его в консоли, скопировал в буфер и начал переписывать в интерфейс. Через час работы выясняется, что поле называется не так, а вложенный объект оказался массивом объектов. Ещё через час выясняется, что в тестовом окружении формат ответа немного другой, и описание снова надо править. Генератор типов делает эту работу за секунду: вставили JSON, получили готовые интерфейсы и вставили их в проект.

Второй сценарий, не менее частый, это работа с конфигурациями и с сохранёнными ответами. Файл конфигурации, выгрузка из базы, сохранённый ответ стороннего сервиса, тестовый фикстур. Всё это JSON, и всё это хочется использовать в TypeScript-проекте с типизацией, чтобы редактор подсказывал поля. Ещё удобнее, когда описание лежит рядом с данными и меняется вместе с ними.

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

  • Описание ответа API, который только что изменился в соседнем сервисе.
  • Типизация конфигурационного файла, который лежит в репозитории.
  • Создание интерфейса для тестовой фикстуры или мока ответа.
  • Разбор структуры вложенного ответа, который трудно прочитать в консоли.
  • Проверка, одинаковы ли по форме объекты в разных ветках ответа.
  • Быстрое получение черновика типа для эксперимента в редакторе.
  • Обновление описания после смены формата ответа у партнёра.
  • Подготовка примера в документации, где нужен читаемый тип данных.

Ошибки, из-за которых типы получаются не такими

Первая ошибка, которая бросается в глаза, это неправильные имена интерфейсов. Инструмент выводит имя по ключу и по привычному правилу убирает множественное число, но правило работает только для английской формы. Ключ `address` превращается в `Addres`, `status` в `Statu`, `series` в `Serie`, потому что буква `s` на конце считается признаком множественного числа. С `categories` при этом получается правильное `Category`, а вот с русскими или неанглийскими ключами будет совсем не то. Имена стоит поправить руками.

Вторая ошибка касается того, что типы получаются обязательными. Каждое поле помечается как присутствующее, хотя в реальном ответе часть полей может отсутствовать. Если поле иногда приходит, а иногда нет, в описании нужно добавить знак вопроса, иначе код будет считать, что значение всегда определено, и упадёт на `undefined` при разборе. Третья ошибка связана с типами-объединениями: если в массиве смешаны разные формы, генератор честно выдаст объединение, но такое объединение в реальном коде приходится сужать вручную, и это нормальная часть работы.

Четвёртая ошибка касается полей со значением `null`. Инструмент превращает его в тип `null`, и это верно, но в реальном ответе поле может быть как `null`, так и обычным значением, и тогда нужен тип вроде `string | null`. Та же история с пустыми массивами: из них выводится `unknown[]`, потому что выводить больше не из чего, и это честнее, чем угадывать. Пятая ошибка, о которой стоит помнить, в том, что инструмент не знает про даты. Строка `2024-03-10` останется строкой `string`, и если в проекте используется `Date`, тип придётся поправить. Перед правкой полезно посмотреть, что вообще пришло, через форматтер JSON.

  • Имена интерфейсов в единственном числе образованы неверно: `address` даёт `Addres`, `status` даёт `Statu`.
  • Все поля помечены как обязательные, хотя часть из них иногда отсутствует.
  • Поле с `null` описано только как `null`, без варианта обычного значения.
  • Пустой массив превращён в `unknown[]` и остался без уточнения.
  • Даты в ответе остались строковым типом, хотя в проекте используется `Date`.
  • Ожидание, что генератор создаст `type` вместо `interface` или добавит `readonly`.
  • Попытка получить перечисление из поля со строковыми значениями: получится обычный `string`.
  • Ожидание, что вложенные объекты с одинаковыми полями получат разные описания. Наоборот, они схлопываются.

Как устроен разбор JSON и что он умеет

Работа начинается с `JSON.parse`, и если текст невалиден, парсер вернёт сообщение об ошибке с указанием позиции, а генерация не запустится. Дальше по разобранной структуре идёт рекурсивный обход. Для каждого значения определяется тип: `null` становится `null`, строки, числа и логические значения получают соответствующие примитивы, а массив превращается в список. Если элементы массива однотипные, получается `T[]`, а если разные, то объединение в скобках, например `(string | number)[]`.

Когда обход доходит до объекта, для него создаётся отдельный интерфейс. Имя берётся из ключа, по которому объект встретился, и переводится в PascalCase: `user_profile` становится `UserProfile`. Ключи, которые не являются корректными идентификаторами TypeScript, берутся в кавычки, как и требует язык. Чтобы одинаковые по форме объекты не плодили копии, для каждого набора полей считается подпись, поля сортируются и складываются в строку, и одинаковые подписи приводят к одному общему интерфейсу.

Отдельная деталь, которую полезно знать, касается корневого элемента. Если в JSON лежит объект, для него создаётся интерфейс с указанным вами именем, по умолчанию `Root`. Если в корне массив, описание принимает вид `export interface Root { value: T[]; }`, то есть массив оборачивается в поле. Если в корне примитив, например число, получится интерфейс с единственным полем `value`. Вложенные интерфейсы выводятся раньше корневого, и все они помечаются словом `export`, чтобы их можно было импортировать по отдельности.

Как разные значения JSON превращаются в типы

Значение в JSONТип в TypeScriptКомментарий
"текст"stringТип определяется по значению
42numberЦелые и дробные дают один тип
truebooleanЛогические значения различаются
nullnullОбъединение с другими типами не создаётся
[1, 2, 3]number[]Однотипные элементы дают простой список
["a", 1]Объединение string и numberСмешанный массив даёт объединение типов в скобках
[]unknown[]Выводить тип не из чего
{...}Отдельный интерфейсИмя берётся из ключа
"key-with-dash""key-with-dash": stringНекорректный идентификатор берётся в кавычки

Одинаковые по набору полей объекты переиспользуют один интерфейс: подпись формы считается из отсортированного списка полей и их типов.

Генерация типов и другие способы описать данные

Генератор по JSON удобен тем, что берёт готовое и правдоподобное описание. Но у него есть альтернативы, и у каждой своя сильная сторона. Написать типы руками можно, и это даже лучше: вы сразу подумаете о необязательных полях, о форматах дат и о том, какие значения вообще допустимы. Цена этого времени, а для рукописных описаний время дороже, чем секунды.

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

Отдельно стоит сказать про конвертацию в другие форматы, потому что JSON часто приходит не в том виде, в котором его хочется видеть. В YAML он становится читаемее, в CSV удобнее разбирать табличные данные, а в JSON Schema превращается в формальное описание, по которому ответ можно валидировать. Для работы с запросами внутри JSON пригодится поиск по пути JSON.

Сравнение способов описать данные

ПодходПлюсыЧем приходится платить
Генерация из JSONБыстро и всегда по фактуОбязательные поля, неверные имена, нет дат
Ручное описаниеПолный контроль над типамиВремя на набор каждого поля
JSON SchemaФормально и проверяемоОтдельный файл и синтаксис схем
Схема как источникОдин источник правдыСхему тоже нужно где-то хранить и поддерживать
Проверка в рантаймеОшибка поймана сразу, до использованияВнешняя библиотека и её настройка
TypeScript как естьНичего лишнегоСлабая гарантия, значения приходят извне
Ручная правка генерацииДоработка точечноКаждое изменение приходится повторять

Перед тем как описывать тип, посмотрите на сам ответ: форматтер JSON покажет его в читаемом виде, а валидатор схемы проверит соответствие.

Приёмы, которые упрощают работу с типами

Сгенерированный тип это черновик, а не готовый результат. Пара приёмов превращают его в то, что действительно можно положить в проект.

  • Сразу задайте имя корневого интерфейса осмысленным, например `User`, вместо значения по умолчанию.
  • Пройдитесь по именам интерфейсов и поправьте те, что образованы неудачно, например `Addres`.
  • Отметьте знаком вопроса поля, которые иногда отсутствуют в ответе.
  • Добавьте `| null` к полям, которые приходят пустыми, иначе тип будет слишком узким.
  • Замените тип дат со `string` на `Date`, если в проекте используется объект Date.
  • Проверьте, не схлопнулись ли разные по смыслу объекты в один интерфейс: одинаковая форма не значит одинаковый смысл.
  • Сохраните полученный тип рядом с тестовой фикстурой, чтобы он не устарел незаметно.

Частые вопросы

Как обрабатываются имена полей?

Имена интерфейсов приводятся к PascalCase, а ключи, которые не являются валидными идентификаторами TypeScript, сохраняются, но берутся в кавычки — ровно так, как этого требует язык.

Что с пустыми массивами и null?

Пустой массив становится unknown[] (вывести тип не из чего), null превращается в тип null, а смешанные массивы дают объединение найденных внутри типов.

Полезные статьи

Похожие инструменты