JSON Toolkit

JSON у TypeScript: інтерфейси, захисники типів і межі генерації

Головне: Генерація інтерфейсу зі зразка JSON — це половина роботи: необов'язкові поля, null, об'єднання, дати й великі цілі числа потребують людського проходу, а типи не дають перевірки під час виконання.

Середній~9 хв читанняОпубліковано 2026-10-31Оновлено 2026-11-04
Обкладинка посібника з JSON в інтерфейс TypeScript і захисників типів

Коротко: згенеруйте інтерфейс, щоб заощадити набирання, потім перевірте необов'язковість і null, залишайте дати й великі цілі числа рядками та додайте перевірку під час виконання на межі; сприймаючи згенеровані типи як контракт, ви не отримуєте жодної перевірки.

Генератор бачить лише один зразок, який ви йому вставили. Чи можуть поля бути відсутні, чи можуть значення бути null і який формат дат — усе це здогади, і виправляти неправильні здогади доведеться вам.

1. Що насправді робить генератор

Вставте зразок JSON у JSON у TypeScript, і отриманий інтерфейс буде підсумком форми: він рахує ключі й типи значень, які побачив, а потім обирає найвужче доступне подання.

{
  "id": 1,
  "email": "ada@example.com",
  "profile": { "nickname": "Ada", "age": 36 },
  "tags": ["admin", "beta"],
  "createdAt": "2026-10-31T08:00:00Z"
}

Вивід виглядає приблизно так:

interface User {
  id: number;
  email: string;
  profile: { nickname: string; age: number };
  tags: string[];
  createdAt: string;
}

Це компілюється, але описує ідеальне значення, а не реальний контракт кінцевої точки. Генератор не знає, чи може email бути відсутнім, чи може tags бути порожнім, чи createdAt завжди несе часовий пояс. Він навіть не знає, що id в інших записах може бути рядком.

Тож правильне очікування таке: генератор заощаджує набирання, людина додає сенс.

2. Необов'язкові поля й null: ? — це не | null

Це та частина згенерованого виводу, яку найчастіше копіюють, і та, що найчастіше ламається.

Форма TypeScript Значення Відповідний JSON
email: string Завжди присутнє, завжди рядок {"email":"a@b.c"}
email?: string Ключ може бути відсутнім; коли присутній, не може бути null {} або {"email":"a@b.c"}
email: string | null Ключ завжди присутній, значення може бути null {"email":null}
email?: string | null Ключ може бути відсутнім і значення може бути null Усі три випадки

Генератор бачив один зразок, тож зазвичай видає першу форму. Якщо API насправді може повертати null, тип бреше: усе компілюється, а потім email.toLowerCase() кидає виняток під час виконання.

Виправити можна двома способами:

  1. Зберіть кілька реальних зразків, що покривають і «ключ відсутній», і «значення null», потім дозвольте генератору вивести знову.
  2. Після генерації змініть поля вручну на ?: T | null і справді обробіть обидві гілки в коді.

Не йдіть шляхом найменшого опору, позначаючи все ?: string. Необов'язкове означає «ключа може не існувати», а не «значення може бути порожнім». undefined і null також поводяться по-різному в JSON.stringify: перший пропускається, другий зберігається як null.

3. Нерівномірні елементи масиву й типи об'єднання

Масиви — найлегше зіпсувати структуру. Якщо tags у трьох записах виглядає як ["admin"], [] і ["beta", 3], генератор може видати string[], (string | number)[] або unknown[] залежно від того, які зразки він бачив.

Людина має вирішити:

  • Типи елементів справді змішані: пишіть (string | number)[] і звужуйте в місці використання.
  • Лише один запис брудний: виправте дані. Не ховайте це за any[].
  • Порожні масиви: генератор не може вивести тип елемента й відкочується до never[] або any[], тож ви мусите його заповнити.
  • Кортежі фіксованої довжини: використовуйте [string, string], лише коли бізнес справді гарантує довжину; інакше додавання елемента згодом стане помилкою компіляції.

Масиви об'єктів теж потребують уваги, коли ключі нерівномірні:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // деякі записи не несуть цього поля
}

Ставте необов'язковість на тип елемента, а не на масив. items?: OrderItem[] означає, що сам масив може бути відсутнім, що є зовсім іншим твердженням.

4. Дати, великі цілі числа, переліки: типи, яких у JSON немає

У JSON лише рядки й числа; бізнес-сенс живе в конвенціях. Генератор виводить із літералів, тож виправляйте вручну:

Літерал JSON Звичайний згенерований вивід Яким він має бути
"2026-10-31T08:00:00Z" string Залишайте string у транспортному шарі й конвертуйте в місці використання; ніколи не оголошуйте Date безпосередньо
1717075200000 number Використовуйте number для мілісекундної мітки часу й документуйте одиницю
"1234567890123456789" string Залишайте string, щоб number не втратив точність
1 і "1" змішані number | string Нормалізуйте до однієї форми; не штовхайте об'єднання в бізнес-код
"admin" string Літеральне об'єднання 'admin' | 'editor' або спільна константа переліку
true і "true" змішані boolean | string Виправте джерело даних, а не генеруйте цей тип

Найважливіше правило: дати в JSON завжди є рядками. Написання createdAt: Date — поширене самообманювання: JSON.parse ніколи не створює Date, тож значення під час виконання все ще рядок, і .getTime() кидає виняток. Залишайте транспортний тип string, явно викликайте new Date(value) на межі й перевіряйте Number.isNaN.

Великі цілі числа працюють так само. Щойно id перевищує 2^53, JSON.parse вже втратив точність, і генерація number лише закріплює баг у типі. Бекенд повертає рядок, фронтенд залишає його рядком — це єдиний надійний варіант.

5. Згенеровані типи не дають перевірки під час виконання

Типи TypeScript стираються під час компіляції. Яким би точним не був інтерфейс, він не може зупинити відповідь {"id": "x"} — він обмежує ваш власний код, а не байти, що надходять по мережі.

/**
 * Check whether a runtime value is a user object.
 * @param value The runtime value to validate
 * @returns Whether it satisfies the minimum shape of User
 */
function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}

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

Практичний компроміс виглядає так:

Підхід Перевірка під час виконання Джерело типів Підходить
Лише інтерфейс Немає Рукописні або згенеровані Довірені внутрішні дані, обмеження під час компіляції
Інтерфейс плюс рукописний захисник Так Два визначення, що розходяться Мало полів, чітка межа
zod / valibot Так Типи, виведені зі схеми Зовнішні API, форми, конфігурація
JSON Schema плюс валідатор Так Типи, згенеровані зі схеми Міжмовні контракти, документація

Якщо дані надходять ззовні, зробіть схему єдиним джерелом істини й виводьте типи через z.infer або json-schema-to-ts. Тоді «перевірка проходить» і «тип правильний» — це один факт, і немає другого визначення, яке може розійтися. Про повний підхід двосторонньої синхронізації дивіться Синхронізація схеми й типів TypeScript.

6. Interface чи type: не сперечайтеся про згенерований вивід

Генератори зазвичай видають interface, але в більшості ситуацій вони взаємозамінні. Насправді важливі три можливості:

  • Злиття оголошень: interface можна оголосити кілька разів, і він зливається автоматично; type не може. Це допомагає під час латання типів третіх сторін, але бізнес-моделі мають цього уникати — злиття дозволяє «зайвим полям» непомітно пройти компілятор.
  • Об'єднання й відображені типи: type може безпосередньо виражати A | B, Partial<T> і шаблонні літеральні типи; interface не може.
  • Рекурсія: обидва підтримують, але складні рекурсивні форми читаються природніше як type.

Корисніше правило команди: використовуйте interface для форм об'єктів, type для об'єднань, службових типів і типів функцій. Залишайте згенерований вивід за замовчуванням і витрачайте час рев'ю на необов'язковість і перевірку під час виконання.

7. Інструментарій: три способи генерації

Підхід Вхід Сильна сторона Обмеження
JSON у TypeScript на сайті Зразок JSON Працює одразу, обробляється локально Лише на основі поточного зразка; потребує ручного звуження
quicktype JSON / JSON Schema Багатомовний вивід, інтеграція з CLI Консервативне виведення, об'єднання можуть бути надто широкими
json-schema-to-typescript JSON Schema Керований контрактом і відтворюваний Спершу потрібна надійна схема

Типовий виклик quicktype:

npx quicktype -s json -o user.ts user.json --just-types

--just-types видає лише типи й не генерує код перетворення під час виконання, що підходить проєктам, які вже мають шар розбору. Якщо схема написана вручну або створена бекендом, json-schema-to-typescript підходить краще, бо він поважає required, enum і additionalProperties — обмеження, яких виведення зі зразків просто не бачить.

Генерація схем має власні пастки, як-от позначення кожного поля обов'язковим або непомітне перетворення null на необов'язковість; дивіться поширені пастки генераторів JSON Schema.

8. Чек-лист перед комітом згенерованих типів

Пройдіться цими п'ятьма пунктами перед комітом виводу:

  1. Нормалізуйте назви полів: не дозволяйте userId і user_id співіснувати; відобразіть один раз у шарі конвертації.
  2. Перевірте необов'язковість: порівняйте з реальними відповідями й знайдіть, які поля можуть бути відсутніми або null.
  3. Залишайте дати й id рядками: документуйте одиницю й формат у типі та розбирайте явно в місці використання.
  4. Зменшуйте any і unknown: скрізь, де генератор видав any, або додайте тип, або залиште unknown і додайте захисник.
  5. Додайте перевірку під час виконання: перевірте зовнішні дані принаймні один раз; не очікуйте, що інтерфейс упіймає брудні дані.

Якщо проєкт використовує кілька бібліотек JSON (JSON.parse, superjson, lossless-json), їхня поведінка з великими цілими числами й датами різниться, тож підтвердьте її під час вибору. Дивіться Вибір бібліотеки розбору JSON.

Чому генератор позначає кожне необов'язкове поле обов'язковим?

Він бачив один зразок, тож кожен ключ, що з'явився, трактується як завжди присутній. Надайте кілька зразків, що покривають різні випадки, або генеруйте з JSON Schema — масив required там є авторитетним джерелом.

Чому оголошення Date в інтерфейсі зазнає збою під час виконання?

JSON.parse створює лише рядки, числа, булеві значення, null, об'єкти й масиви; він ніколи не конструює Date. Оголошення Date — це обіцянка під час компіляції, тоді як значення під час виконання залишається рядком. Залишайте string у транспортному шарі й конвертуйте на межі.

Чи можна використовувати згенеровані типи для перевірки?

Ні. Типи TypeScript стираються після компіляції й не накладають нічого на дані під час виконання. Перевіряйте зовнішні дані через zod, valibot, ajv або рукописний захисник типу.

Великий цілий id має бути number чи string?

Якщо він може перевищувати 2^53, він має бути string. JSON.parse втрачає точність під час розбору, а типізація як number лише записує баг у тип, де його вже не виправити.