JSON Toolkit

JSON ke TypeScript: Interface, Type Guard, dan Batas Generasi

Intinya: Menghasilkan interface dari sampel JSON hanyalah separuh pekerjaan: field opsional, null, union, tanggal, dan integer besar memerlukan tinjauan manusia, dan tipe tidak melakukan validasi runtime.

Menengah±9 menit bacaDiterbitkan 2026-10-31Diperbarui 2026-11-04
Sampul untuk panduan JSON ke TypeScript interface dan type guard

Intinya: hasilkan interface untuk menghemat pengetikan, lalu periksa ulang opsionalitas dan null, pertahankan tanggal dan integer besar sebagai string, dan tambahkan validasi runtime di batas; memperlakukan tipe hasil generasi sebagai kontrak berarti Anda tidak mendapat validasi sama sekali.

Generator hanya melihat satu sampel yang Anda tempelkan padanya. Apakah field bisa hilang, apakah nilai bisa null, dan format apa yang dipakai tanggal — semuanya tebakan, dan tebakan yang salah adalah tanggung jawab Anda untuk memperbaikinya.

1. Apa yang sebenarnya dilakukan generator

Tempelkan sampel JSON ke JSON ke TypeScript dan interface yang Anda dapatkan kembali adalah ringkasan bentuk: ia menghitung kunci dan tipe nilai yang pernah dilihatnya, lalu memilih representasi tersempit yang tersedia.

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

Outputnya kira-kira seperti ini:

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

Ini bisa dikompilasi, tetapi mendeskripsikan nilai ideal, bukan kontrak nyata endpoint. Generator tidak tahu apakah email bisa hilang, apakah tags bisa kosong, atau apakah createdAt selalu membawa zona waktu. Ia bahkan tidak tahu bahwa id mungkin berupa string di record lain.

Jadi ekspektasi yang tepat adalah: generator menghemat pengetikan, manusia menyediakan makna.

2. Field opsional dan null: ? bukan | null

Ini adalah bagian output generasi yang paling sering disalin, dan bagian yang paling sering rusak.

Bentuk TypeScript Arti JSON yang cocok
email: string Selalu ada, selalu string {"email":"a@b.c"}
email?: string Kunci boleh tidak ada; jika ada tidak boleh null {} atau {"email":"a@b.c"}
email: string | null Kunci selalu ada, nilai boleh null {"email":null}
email?: string | null Kunci boleh tidak ada dan nilai boleh null Ketiga kasus

Generator hanya melihat satu sampel, jadi biasanya mengeluarkan bentuk pertama. Jika API sebenarnya dapat mengembalikan null, tipenya berbohong: semuanya terkompilasi, lalu email.toLowerCase() melempar error saat runtime.

Ada dua cara memperbaikinya:

  1. Kumpulkan beberapa sampel nyata yang mencakup "kunci hilang" dan "nilai null", lalu biarkan generator menyimpulkan lagi.
  2. Setelah generasi, ubah field secara manual menjadi ?: T | null dan benar-benar tangani kedua cabang di kode.

Jangan mengambil jalan pintas dengan menandai semuanya ?: string. Opsional berarti "kunci mungkin tidak ada", bukan "nilai mungkin kosong". undefined dan null juga berperilaku berbeda di JSON.stringify: yang pertama dihilangkan, yang terakhir dipertahankan sebagai null.

3. Elemen array yang tidak seragam dan tipe union

Array adalah struktur yang paling mudah salah. Jika tags muncul sebagai ["admin"], [], dan ["beta", 3] di tiga record, generator dapat mengeluarkan string[], (string | number)[], atau unknown[], tergantung sampel mana yang dilihatnya.

Manusia harus memutuskan:

  • Tipe elemen memang campuran: tulis (string | number)[] dan lakukan narrowing di titik penggunaan.
  • Hanya satu record yang kotor: perbaiki datanya. Jangan menyembunyikannya dengan any[].
  • Array kosong: generator tidak dapat menyimpulkan tipe elemen dan mundur ke never[] atau any[], sehingga Anda harus mengisinya.
  • Tuple dengan panjang tetap: gunakan [string, string] hanya jika bisnis benar-benar menjamin panjangnya; jika tidak, menambahkan elemen nanti menjadi compile error.

Array object juga perlu perhatian saat kuncinya tidak seragam:

interface OrderItem {
  sku: string;
  quantity: number;
  discount?: number; // some records do not carry this field
}

Letakkan opsionalitas pada tipe elemen, bukan pada array. items?: OrderItem[] berarti array itu sendiri mungkin tidak ada, yang merupakan pernyataan yang sama sekali berbeda.

4. Tanggal, integer besar, enum: tipe yang tidak dimiliki JSON

JSON hanya punya string dan number; makna bisnis hidup dalam konvensi. Generator menyimpulkan dari literal, jadi koreksi secara manual:

Literal JSON Output generasi yang umum Seharusnya
"2026-10-31T08:00:00Z" string Pertahankan string di lapisan transport dan konversi di titik penggunaan; jangan pernah mendeklarasikan Date secara langsung
1717075200000 number Gunakan number untuk timestamp milidetik dan dokumentasikan unitnya
"1234567890123456789" string Pertahankan sebagai string agar number tidak kehilangan presisi
1 dan "1" bercampur number | string Normalisasi ke satu bentuk; jangan mendorong union itu ke kode bisnis
"admin" string Union literal 'admin' | 'editor', atau konstanta enum bersama
true dan "true" bercampur boolean | string Perbaiki sumber datanya alih-alih menghasilkan tipe ini

Aturan terpenting: tanggal dalam JSON selalu string. Menulis createdAt: Date adalah penipuan diri yang umum — JSON.parse tidak pernah membangun Date, sehingga nilai runtime tetap string dan .getTime() melempar error. Pertahankan tipe transport sebagai string, panggil new Date(value) secara eksplisit di batas, dan periksa Number.isNaN.

Integer besar bekerja dengan cara yang sama. Begitu id melebihi 2^53, JSON.parse sudah kehilangan presisi, dan menghasilkan number hanya menyemen bug ke dalam tipe. Backend mengembalikan string, frontend mempertahankannya sebagai string; itulah satu-satunya opsi yang andal.

5. Tipe hasil generasi tidak melakukan validasi runtime

Tipe TypeScript dihapus saat kompilasi. Sekalipun interface-nya sangat tepat, ia tidak dapat menghentikan respons {"id": "x"} — ia membatasi kode Anda sendiri, bukan byte yang tiba melalui jaringan.

/**
 * 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';
}

Type guard hanya mencakup field yang Anda tulis ke dalam pemeriksaan. Saat field bertambah, guard tulisan tangan menjadi definisi kedua yang harus dipelihara, menjauh dari interface.

Trade-off praktisnya terlihat seperti ini:

Pendekatan Validasi runtime Sumber tipe Cocok untuk
Interface saja Tidak ada Ditulis tangan atau dihasilkan Data internal tepercaya, batasan waktu kompilasi
Interface plus guard tulisan tangan Ya Dua definisi yang menyimpang Sedikit field, batas yang jelas
zod / valibot Ya Tipe disimpulkan dari schema API eksternal, form, konfigurasi
JSON Schema plus validator Ya Tipe dihasilkan dari schema Kontrak lintas bahasa, dokumentasi

Jika data berasal dari luar, jadikan schema sebagai satu-satunya sumber kebenaran dan turunkan tipe dengan z.infer atau json-schema-to-ts. Kemudian "validasi lolos" dan "tipenya benar" adalah fakta yang sama, dan tidak ada definisi kedua yang menyimpang. Untuk pendekatan sinkronisasi dua arah secara lengkap, lihat Sinkronisasi schema dan tipe TypeScript.

6. Interface atau type: jangan berdebat soal output generasi

Generator biasanya mengeluarkan interface, tetapi keduanya dapat dipertukarkan dalam sebagian besar situasi. Yang benar-benar penting adalah tiga kemampuan:

  • Declaration merging: interface dapat dideklarasikan beberapa kali dan bergabung otomatis; type tidak bisa. Itu membantu saat menambal tipe pihak ketiga, tetapi model bisnis harus menghindarinya — penggabungan membiarkan "field tambahan" lolos dari compiler tanpa disadari.
  • Union dan mapped type: type dapat mengekspresikan A | B, Partial<T>, dan template literal type secara langsung; interface tidak bisa.
  • Rekursi: keduanya mendukungnya, tetapi bentuk rekursif yang kompleks lebih terbaca alami sebagai type.

Aturan tim yang lebih berguna: gunakan interface untuk bentuk object, type untuk union, utility type, dan function type. Biarkan output generasi pada default-nya dan habiskan waktu tinjauan pada opsionalitas dan validasi runtime.

7. Toolchain: tiga cara menghasilkan

Pendekatan Input Keunggulan Keterbatasan
JSON ke TypeScript di situs ini Sampel JSON Langsung berfungsi, diproses secara lokal Hanya berdasarkan sampel saat ini; memerlukan narrowing manual
quicktype JSON / JSON Schema Output multi-bahasa, integrasi CLI Inferensi konservatif, union bisa terlalu lebar
json-schema-to-typescript JSON Schema Berbasis kontrak dan dapat direproduksi Memerlukan schema yang tepercaya lebih dulu

Contoh pemanggilan quicktype yang umum:

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

--just-types hanya mengeluarkan tipe dan tanpa kode konversi runtime, yang cocok untuk proyek yang sudah memiliki lapisan parsing. Jika schema ditulis tangan atau dihasilkan backend, json-schema-to-typescript lebih cocok karena menghormati required, enum, dan additionalProperties — batasan yang tidak dapat dilihat oleh inferensi sampel.

Generasi schema punya jebakannya sendiri, seperti menandai setiap field sebagai required atau diam-diam mengubah null menjadi opsional; lihat jebakan umum generator JSON Schema.

8. Daftar periksa sebelum commit tipe hasil generasi

Jalani lima item ini sebelum Anda commit output-nya:

  1. Normalisasi nama field: jangan biarkan userId dan user_id hidup berdampingan; petakan sekali di lapisan konversi.
  2. Periksa ulang opsionalitas: bandingkan dengan respons nyata dan temukan field mana yang bisa hilang atau null.
  3. Pertahankan tanggal dan id sebagai string: dokumentasikan unit dan format dalam tipe, dan parse secara eksplisit di titik penggunaan.
  4. Kurangi any dan unknown: di mana pun generator mengeluarkan any, entah tambahkan tipe atau pertahankan unknown dan tambahkan guard.
  5. Tambahkan validasi runtime: validasi data eksternal setidaknya sekali; jangan berharap interface menangkap data buruk.

Jika proyek memakai beberapa pustaka JSON (JSON.parse, superjson, lossless-json), perilaku mereka untuk integer besar dan tanggal berbeda, jadi pastikan saat pemilihan. Lihat Pemilihan pustaka parsing JSON.

Mengapa generator menandai setiap field opsional sebagai required?

Ia hanya melihat satu sampel, sehingga setiap kunci yang muncul diperlakukan sebagai selalu ada. Berikan beberapa sampel yang mencakup kasus berbeda, atau hasilkan dari JSON Schema — array required di sana adalah sumber otoritatif.

Mengapa mendeklarasikan Date dalam interface gagal saat runtime?

JSON.parse hanya menghasilkan string, number, boolean, null, object, dan array; ia tidak pernah membangun Date. Mendeklarasikan Date adalah janji waktu kompilasi sementara nilai runtime tetap string. Pertahankan string di lapisan transport dan konversi di batas.

Bisakah tipe hasil generasi digunakan sebagai validasi?

Tidak. Tipe TypeScript dihapus setelah kompilasi dan tidak membebankan apa pun pada data runtime. Validasi data eksternal dengan zod, valibot, ajv, atau type guard tulisan tangan.

Haruskah id integer besar ditipekan sebagai number atau string?

Jika dapat melebihi 2^53, harus string. JSON.parse kehilangan presisi selama parsing, dan mengetikkannya sebagai number hanya menulis bug ke dalam tipe di mana ia tidak dapat diperbaiki lagi.