JSON kepada TypeScript: Antara Muka, Type Guard, dan Had Penjanaan
Inti pati: Menjana antara muka daripada sampel JSON hanyalah separuh kerja: medan pilihan, null, gabungan, tarikh dan integer besar memerlukan semakan manusia, dan jenis tidak melakukan pengesahan masa jalan.

Kesimpulan utama: jana antara muka untuk menjimatkan penulisan, kemudian semak semula pilihan dan null, kekalkan tarikh serta integer besar sebagai rentetan, dan tambah pengesahan masa jalan pada sempadan; melayan jenis yang dijana sebagai kontrak tidak memberikan anda sebarang pengesahan.
Penjana hanya melihat satu sampel yang anda tampalkan. Sama ada medan boleh hilang, sama ada nilai boleh menjadi null, dan format apa yang digunakan oleh tarikh semuanya tekaan — dan tekaan yang salah adalah tanggungjawab anda untuk dibetulkan.
1. Apa yang sebenarnya dilakukan oleh penjana
Tampal sampel JSON ke dalam JSON kepada TypeScript dan antara muka yang anda dapat kembali ialah ringkasan bentuk: ia mengira kunci dan jenis nilai yang telah dilihatnya, kemudian memilih perwakilan paling sempit yang tersedia.
{
"id": 1,
"email": "ada@example.com",
"profile": { "nickname": "Ada", "age": 36 },
"tags": ["admin", "beta"],
"createdAt": "2026-10-31T08:00:00Z"
}
Outputnya kelihatan lebih kurang seperti ini:
interface User {
id: number;
email: string;
profile: { nickname: string; age: number };
tags: string[];
createdAt: string;
}
Ini dikompil, tetapi ia menerangkan nilai ideal, bukan kontrak sebenar titik akhir itu. Penjana tidak tahu sama ada email boleh hilang, sama ada tags boleh kosong, atau sama ada createdAt sentiasa membawa zon waktu. Ia bahkan tidak tahu bahawa id mungkin rentetan dalam rekod lain.
Jadi jangkaan yang betul ialah: penjana menjimatkan penulisan, manusia membekalkan makna.
2. Medan pilihan dan null: ? bukan | null
Inilah bahagian output yang dijana yang paling kerap disalin, dan bahagian yang paling kerap rosak.
| Bentuk TypeScript | Makna | JSON yang sepadan |
|---|---|---|
email: string |
Sentiasa wujud, sentiasa rentetan | {"email":"a@b.c"} |
email?: string |
Kunci mungkin tidak hadir; apabila hadir ia tidak boleh null | {} atau {"email":"a@b.c"} |
email: string | null |
Kunci sentiasa wujud, nilai mungkin null | {"email":null} |
email?: string | null |
Kunci mungkin tidak hadir dan nilai mungkin null | Ketiga-tiga kes |
Penjana telah melihat satu sampel, jadi ia biasanya mengeluarkan bentuk pertama. Jika API sebenarnya boleh memulangkan null, jenis itu berbohong: semuanya dikompil, dan kemudian email.toLowerCase() melambung pada masa jalan.
Terdapat dua cara untuk membetulkannya:
- Kumpulkan beberapa sampel sebenar yang merangkumi kedua-dua "kunci tiada" dan "nilai ialah null", kemudian biarkan penjana membuat inferens semula.
- Selepas menjana, ubah medan dengan tangan kepada
?: T | nulldan benar-benar kendalikan kedua-dua cabang dalam kod.
Jangan ambil jalan pintas menandakan semuanya ?: string. Pilihan bermaksud "kunci mungkin tidak wujud", bukan "nilai mungkin kosong". undefined dan null juga berkelakuan berbeza dalam JSON.stringify: yang pertama ditinggalkan, yang kedua dikekalkan sebagai null.
3. Elemen tatasusunan yang tidak sekata dan jenis gabungan
Tatasusunan ialah struktur yang paling mudah salah. Jika tags muncul sebagai ["admin"], [], dan ["beta", 3] merentas tiga rekod, penjana mungkin mengeluarkan string[], (string | number)[], atau unknown[], bergantung pada sampel yang dilihatnya.
Manusia mesti memutuskan:
- Jenis elemen memang bercampur: tulis
(string | number)[]dan sempitkan pada titik penggunaan. - Hanya satu rekod kotor: betulkan data itu. Jangan sembunyikannya dengan
any[]. - Tatasusunan kosong: penjana tidak dapat menyimpulkan jenis elemen dan beralih kepada
never[]atauany[], jadi anda mesti mengisinya. - Tuple panjang tetap: gunakan
[string, string]hanya apabila perniagaan benar-benar menjamin panjangnya; jika tidak, menambah elemen kemudian menjadi kesilapan kompil.
Tatasusunan objek juga memerlukan perhatian apabila kunci tidak sekata:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // some records do not carry this field
}
Letakkan pilihan pada jenis elemen, bukan pada tatasusunan. items?: OrderItem[] bermaksud tatasusunan itu sendiri mungkin tidak hadir, yang sama sekali pernyataan berbeza.
4. Tarikh, integer besar, enum: jenis yang tiada dalam JSON
JSON hanya mempunyai rentetan dan nombor; makna perniagaan hidup dalam konvensyen. Penjana membuat inferens daripada literal, jadi betulkannya dengan tangan:
| Literal JSON | Output yang biasa dijana | Apa yang sepatutnya |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
Kekalkan string dalam lapisan pengangkutan dan tukar pada titik penggunaan; jangan sekali-kali mengisytiharkan Date secara terus |
1717075200000 |
number |
Gunakan number untuk cap masa milisaat dan dokumenkan unitnya |
"1234567890123456789" |
string |
Kekalkan sebagai string supaya number tidak kehilangan ketepatan |
1 dan "1" bercampur |
number | string |
Normalkan kepada satu bentuk; jangan mendorong gabungan itu ke dalam kod perniagaan |
"admin" |
string |
Gabungan literal 'admin' | 'editor', atau pemalar enum yang dikongsi |
true dan "true" bercampur |
boolean | string |
Betulkan sumber data dan jangan menjana jenis ini |
Peraturan paling penting: tarikh dalam JSON sentiasa rentetan. Menulis createdAt: Date ialah penipuan diri yang lazim — JSON.parse tidak pernah membina Date, jadi nilai masa jalan masih rentetan dan .getTime() melambung. Kekalkan jenis pengangkutan sebagai string, panggil new Date(value) secara eksplisit pada sempadan, dan semak Number.isNaN.
Integer besar berfungsi dengan cara yang sama. Sebaik sahaja id melebihi 2^53, JSON.parse sudah kehilangan ketepatan, dan menjana number hanya mengukuhkan bug itu ke dalam jenis. Bahagian belakang memulangkan rentetan, frontend mengekalkannya sebagai rentetan; itulah satu-satunya pilihan yang boleh dipercayai.
5. Jenis yang dijana tidak melakukan pengesahan masa jalan
Jenis TypeScript dipadamkan pada masa kompil. Tidak kira betapa tepat antara muka itu, ia tidak dapat menghalang respons {"id": "x"} — ia mengekang kod anda sendiri, bukan bait yang tiba melalui rangkaian.
/**
* 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 merangkumi medan yang anda tulis ke dalam semakan itu. Apabila medan bertambah, type guard tulisan tangan menjadi definisi kedua untuk diselenggara, hanyut jauh daripada antara muka.
Pertukaran praktikalnya kelihatan seperti ini:
| Pendekatan | Pengesahan masa jalan | Sumber jenis | Sesuai |
|---|---|---|---|
| Antara muka sahaja | Tiada | Ditulis tangan atau dijana | Data dalaman yang dipercayai, kekangan masa kompil |
| Antara muka ditambah type guard tulisan tangan | Ya | Dua definisi yang hanyut | Sedikit medan, sempadan yang jelas |
| zod / valibot | Ya | Jenis disimpulkan daripada skema | API luaran, borang, konfigurasi |
| JSON Schema ditambah pengesah | Ya | Jenis dijana daripada skema | Kontrak merentas bahasa, dokumentasi |
Jika data datang dari luar, jadikan skema sebagai satu-satunya sumber kebenaran dan terbitkan jenis dengan z.infer atau json-schema-to-ts. Kemudian "pengesahan lulus" dan "jenis itu betul" ialah fakta yang sama, dan tiada definisi kedua untuk hanyut. Untuk pendekatan penyegerakan dua hala sepenuhnya, lihat Penyegerakan Schema dan jenis TypeScript.
6. Antara muka atau type: jangan berdebat tentang output yang dijana
Penjana biasanya mengeluarkan interface, tetapi kedua-duanya boleh ditukar ganti dalam kebanyakan situasi. Apa yang sebenarnya penting ialah tiga keupayaan:
- Pencantuman pengisytiharan:
interfaceboleh diisytiharkan berbilang kali dan bergabung secara automatik;typetidak boleh. Itu membantu semasa menampal-nampal jenis pihak ketiga, tetapi model perniagaan harus mengelakkannya — pencantuman membiarkan "medan tambahan" menyelinap melalui pengkompil tanpa disedari. - Gabungan dan jenis terpeta:
typeboleh menyatakanA | B,Partial<T>, dan jenis literal templat secara terus;interfacetidak boleh. - Rekursi: kedua-duanya menyokongnya, tetapi bentuk rekursif yang kompleks lebih semula jadi dibaca sebagai
type.
Peraturan pasukan yang lebih berguna: gunakan interface untuk bentuk objek, type untuk gabungan, jenis utiliti, dan jenis fungsi. Biarkan output yang dijana pada lalainya dan luangkan masa semakan pada pilihan serta pengesahan masa jalan sebagai ganti.
7. Rantaian alat: tiga cara untuk menjana
| Pendekatan | Input | Kekuatan | Had |
|---|---|---|---|
| JSON kepada TypeScript laman ini | Sampel JSON | Berfungsi serta-merta, diproses secara setempat | Berdasarkan sampel semasa sahaja; memerlukan penyempitan manual |
| quicktype | JSON / JSON Schema | Output berbilang bahasa, integrasi CLI | Inferens konservatif, gabungan boleh terlalu luas |
| json-schema-to-typescript | JSON Schema | Dipacu kontrak dan boleh dihasilkan semula | Memerlukan skema yang boleh dipercayai dahulu |
Seruan quicktype yang tipikal:
npx quicktype -s json -o user.ts user.json --just-types
--just-types mengeluarkan jenis sahaja dan tiada kod penukaran masa jalan, yang sesuai untuk projek yang sudah mempunyai lapisan penghuraian. Jika skema ditulis tangan atau dihasilkan oleh bahagian belakang, json-schema-to-typescript lebih sesuai kerana ia menghormati required, enum, dan additionalProperties — kekangan yang tidak dapat dilihat oleh inferens sampel.
Penjanaan skema mempunyai perangkapnya sendiri, seperti menandakan setiap medan sebagai required atau menukar null menjadi pilihan secara senyap; lihat perangkap lazim penjana JSON Schema.
8. Senarai semak sebelum commit jenis yang dijana
Lalui lima item ini sebelum anda commit output itu:
- Normalkan nama medan: jangan biarkan
userIddanuser_idwujud bersama; petakan sekali dalam lapisan penukaran. - Semak semula pilihan: bandingkan dengan respons sebenar dan cari medan mana yang boleh hilang atau
null. - Kekalkan tarikh dan id sebagai rentetan: dokumenkan unit dan format dalam jenis, dan hurai secara eksplisit pada titik penggunaan.
- Kurangkan
anydanunknown: di mana sahaja penjana mengeluarkanany, sama ada tambah jenis atau kekalkanunknowndan tambah type guard. - Tambah pengesahan masa jalan: sahkan data luaran sekurang-kurangnya sekali; jangan jangkakan antara muka menangkap data buruk.
Jika projek menggunakan beberapa pustaka JSON (JSON.parse, superjson, lossless-json), tingkah laku mereka untuk integer besar dan tarikh berbeza, jadi sahkan semasa pemilihan. Lihat Pemilihan pustaka penghuraian JSON.
Mengapa penjana menandakan setiap medan pilihan sebagai required?
Ia melihat satu sampel, jadi setiap kunci yang muncul dilayan sebagai sentiasa hadir. Berikan berbilang sampel yang merangkumi kes berbeza, atau jana daripada JSON Schema — tatasusunan required di sana ialah sumber autoritatif.
Mengapa mengisytiharkan Date dalam antara muka gagal pada masa jalan?
JSON.parse hanya menghasilkan rentetan, nombor, boolean, null, objek, dan tatasusunan; ia tidak pernah membina Date. Mengisytiharkan Date ialah janji masa kompil manakala nilai masa jalan kekal rentetan. Kekalkan string dalam lapisan pengangkutan dan tukar pada sempadan.
Bolehkah jenis yang dijana digunakan sebagai pengesahan?
Tidak. Jenis TypeScript dipadamkan selepas pengkompilan dan tidak mengenakan apa-apa pada data masa jalan. Sahkan data luaran dengan zod, valibot, ajv, atau type guard tulisan tangan.
Patutkah id integer besar ditaip sebagai number atau string?
Jika ia boleh melebihi 2^53, ia mesti string. JSON.parse kehilangan ketepatan semasa penghuraian, dan menaipnya sebagai number hanya menulis bug itu ke dalam jenis di mana ia tidak lagi boleh dibetulkan.