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.

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:
- Kumpulkan beberapa sampel nyata yang mencakup "kunci hilang" dan "nilai null", lalu biarkan generator menyimpulkan lagi.
- Setelah generasi, ubah field secara manual menjadi
?: T | nulldan 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[]atauany[], 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:
interfacedapat dideklarasikan beberapa kali dan bergabung otomatis;typetidak 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:
typedapat mengekspresikanA | B,Partial<T>, dan template literal type secara langsung;interfacetidak 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:
- Normalisasi nama field: jangan biarkan
userIddanuser_idhidup berdampingan; petakan sekali di lapisan konversi. - Periksa ulang opsionalitas: bandingkan dengan respons nyata dan temukan field mana yang bisa hilang atau
null. - Pertahankan tanggal dan id sebagai string: dokumentasikan unit dan format dalam tipe, dan parse secara eksplisit di titik penggunaan.
- Kurangi
anydanunknown: di mana pun generator mengeluarkanany, entah tambahkan tipe atau pertahankanunknowndan tambahkan guard. - 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.