JSON в TypeScript
Генерирует интерфейсы TypeScript из JSON: элементы массива объединены, необязательные ключи, union смешанных типов, null отдельно — всё в браузере.
export interface Root {
id: number
name: string
active: boolean
address: Address
roles: string[]
posts: Post[]
tags: unknown[]
}
export interface Address {
city: string
zip: null
}
export interface Post {
id: number
title: string
views: number | string
pinned?: boolean
}
Интерфейсы: 3
Типы выводятся из данных, а не угадываются
JSON-документ не несёт собственных типов — он несёт значения, и типы приходится вычитывать из них обратно. Для одного объекта это легко, а для коллекции удивительно тонко: нужная вам форма — это не форма какой-то одной записи, а форма, которую должна удовлетворять каждая запись. Этот инструмент выводит интерфейсы TypeScript из образца JSON, рассматривая всё, что образец содержит, так что получаемые типы описывают все ваши данные, а не первую строку, оказавшуюся сверху.
Преобразование идёт только в одну сторону. Превращение типов обратно в JSON означало бы выдумывание значений, а здесь цель обратная: вставленный документ — источник истины, и каждое объявление выводится из него. Вставьте ответ API, файл конфигурации или строку лога и прочитайте интерфейс, который иначе написали бы вручную.
Как объединяются массивы
Все интересные решения происходят на массивах. Наивный конвертер смотрит на первый элемент и останавливается, из-за чего каждое поле, которое он случайно видит, выглядит обязательным, а каждое поле, которого он не видит, пропускается. Этот инструмент вместо этого объединяет все элементы в один тип, и отсюда следуют три вещи:
- Ключ, присутствующий в одних элементах, но отсутствующий в других, становится необязательным, записанным со знаком вопроса. Если у половины ваших записей есть "middleName", а у половины нет, поле — это "middleName?", ровно то, что потребитель должен обработать.
- Ключ, значение которого различается по типу между элементами, становится union. Поле, которое в одной записи число, а в другой строка, типизируется как "number | string" — не ради аккуратности, а потому что это то, что данные действительно содержат и что ваш код должен принимать.
- Вложенный объект внутри элементов объединяется так же, рекурсивно, и выносится в собственный интерфейс. Десять элементов массива, каждый из которых несёт "address", дают один интерфейс Address, описывающий все десять.
Когда сам документ — массив на верхнем уровне, корень становится псевдонимом на интерфейс элемента — например, "type Root = RootItem[]" — с объединённым типом элемента, записанным ниже.
Null, необязательность и почему они различны
Заманчиво трактовать null так же, как отсутствующий ключ, и это ошибка. В TypeScript "name?: string" означает, что свойство может отсутствовать; "name: string | null" означает, что оно всегда есть, но может содержать null. Это разные контракты, и потребитель проверяет их по-разному — "in" против сравнения значения. Этот инструмент держит их порознь: явный null в данных становится членом union "| null", помещённым последним, чтобы "string | null" читалось как ожидается, тогда как ключ, просто отсутствующий в части записей, становится необязательным. Поле, которое и то и другое — null в одной записи, отсутствует в другой — выходит как и то и другое, "field?: T | null", потому что оба факта верны для ваших данных.
Вложенные объекты становятся именованными интерфейсами
Вместо того чтобы встраивать вложенную форму внутрь родителя, каждый объект выносится в собственный интерфейс, имя которого выводится из ключа, под которым он находится. Объект "user" становится интерфейсом User; "address" внутри него становится интерфейсом Address, на который ссылается User. Глубоко встроенные типы трудно читать и невозможно переиспользовать, а именованные интерфейсы — это то, что вы написали бы сами. Элементы массива ставятся в единственное число, где можно — "users" даёт User, "categories" — Category — а ключ, который не образует множественное число, получает суффикс Item, чтобы у элемента было собственное имя.
Если два разных объекта получили бы одно имя — два несвязанных "data", скажем — второй получает суффикс, а не объединяется, так что различные формы остаются различными. Корневой объект выводится первым, и вы можете его переименовать; выбор между выводом "interface" и "type" — это переключатель, поскольку некоторые кодовые базы всюду предпочитают псевдонимы типов.
Откат к unknown
Некоторые значения не несут никакой информации о типе. Пустой массив мог бы содержать что угодно; у пустого объекта нет ключей, которые можно описать. Вместо того чтобы прибегать к "any" — который отключает проверку типов для всего последующего — инструмент откатывается к "unknown": пустой массив становится "unknown[]", пустой объект становится "Record<string, unknown>". Разница важна. "any" молча пропускает ошибки; "unknown" заставляет потребителя сузить значение перед использованием, так что выведенный тип остаётся честным относительно того, что образец сказал и чего не сказал.
Это типы, которые вы ужесточили бы вручную, как только узнаете, что должна содержать пустая коллекция — но пока данные этого не скажут, "unknown" — честный ответ, и именно он сохраняет остальные ваши типы безопасными.
На чём он работает и где
Всё происходит в вашем браузере. JSON разбирается, и типы выводятся на вашем собственном устройстве; ничто из вставленного не загружается, не сохраняется и не логируется. Это делает инструмент безопасным для настоящего ответа API или файла конфигурации с секретами внутри: образец никогда не покидает страницу. Вывод — это обычный TypeScript, который можно вставить прямо в файл "d.ts" или модуль, поправить те немногие поля "unknown", которые данные не смогли описать, и использовать.
Частые вопросы
- Отправляется ли мой JSON на сервер?
- Нет. Документ разбирается, и типы выводятся полностью в вашем браузере, и ничто из вставленного не загружается и не логируется. Безопасно использовать на настоящем ответе API или файле конфигурации.
- Почему поле необязательное, если оно присутствует в моём образце?
- Потому что оно отсутствует хотя бы в одном элементе массива, который инструмент объединил. Вывод читает каждый элемент, а не только первый, так что ключ, который часть записей опускает, становится необязательным — это тип, который ваши данные действительно допускают, даже если запись, на которую вы смотрели, случайно его включала.
- Почему поле стало union вроде string | number?
- Потому что у значения были разные типы в разных элементах массива — строка в одной записи и число в другой. Объединённый тип должен принимать оба, поэтому он записан как union. Если это вас удивляет, обычно это означает, что данные менее однородны, чем ожидалось, и это стоит знать.
- Почему инструмент использует unknown вместо any?
- Для значений, которые он не может описать — пустой массив, пустой объект — "unknown" сохраняет результат типобезопасным, заставляя потребителя сузить значение перед использованием, тогда как "any" отключил бы проверку типов. Вы можете ужесточить эти поля вручную, как только узнаете, что содержит пустая коллекция.
- В чём разница между выводом interface и type?
- Никакой в типах, которые они описывают — оба дают одни и те же формы. "interface" — общепринятая идиома для типов объектов, её можно расширять и объединять; псевдонимы "type" — то, что некоторые кодовые базы предпочитают использовать всюду. Переключатель нужен, чтобы вывод соответствовал стилю вашего проекта.
- Может ли он превратить TypeScript обратно в JSON?
- Нет, и намеренно. Преобразование одностороннее: JSON на входе, типы на выходе. Идти в обратную сторону означало бы выдумывать значения, которых никогда не было в ваших данных, а весь смысл в том, что каждое объявление выводится из того, что вы действительно вставили.
- Как именуются вложенные объекты?
- По ключу, под которым они находятся: объект "user" становится User, "address" внутри него становится Address. Элементы массива ставятся в единственное число, где можно — "categories" даёт Category — а ключ, который не образует множественное число, получает суффикс Item. Две разные формы, которые столкнулись бы на имени, получают суффикс, а не объединяются, так что остаются различными.