Data yang datang dari luar aplikasi tidak pernah membawa tipenya, sehingga batas sistem menjadi titik yang paling perlu dijaga. Topik ini membahas pengetikan fetch dan responsnya, penguraian (parsing) payload, serta perlakuan masukan sebagai unknown di batas sistem. Respons API fiktif sederhana pada contoh dimodelkan sebagai string JSON lokal, sehingga seluruh kode dapat dieksekusi tanpa jaringan.
fetch dan tipe Response#
Fungsi fetch mengambil sumber daya dari jaringan dan mengembalikan Promise<Response>. Objek Response menyimpan status HTTP pada status dan tanda keberhasilan pada ok, sedangkan tubuh respons diurai lewat metode json() yang mengembalikan Promise<any>. Tipe any berarti compiler memercayai bentuk yang ditulis pemanggil, tanpa satu pun pemeriksaan.
Nilai ok bernilai true hanya untuk kode status 200 sampai 299, sedangkan respons dengan status 404 atau 500 tetap menyelesaikan janjinya, bukan menolaknya. Pemeriksaan respons.ok sebelum penguraian dengan demikian wajib dilakukan. Tanpa pemeriksaan itu, tubuh pesan kesalahan server dapat diurai seolah-olah payload yang diharapkan.
Berkas berikut melakukan panggilan jaringan sungguhan, sehingga tidak dapat dieksekusi di lingkungan tanpa akses jaringan dan sengaja tidak disertai keluaran. Polanya tetap layak dicermati, karena data.suhu lolos kompilasi tanpa ada jaminan bidang itu benar-benar ada.
// tidak-dijalankan
const respons = await fetch("https://api.contoh.example/suhu/terakhir");
const data = await respons.json();
console.log(data.suhu);Baris await respons.json() menghasilkan nilai bertipe any, dan pada titik itu seluruh pemeriksaan bentuk berhenti. TypeScript tidak dapat menolak data.suhu maupun data.takAda, karena tipenya tidak menyatakan apa pun. Cara menjaga batas sistem adalah mengubah nilai tersebut menjadi unknown, kemudian memvalidasinya sebelum dipakai.
Payload sebagai unknown#
Payload yang diurai dari teks JSON dapat berupa objek, array, string, angka, boolean, atau null, dan tipenya bergantung pada isi teks tersebut. Tipe unknown menyatakan kenyataan itu, yaitu nilainya ada, tetapi bentuknya belum diketahui. Berbeda dari any, tipe unknown melarang akses properti apa pun sebelum tipenya dipersempit. Pemilihan tipe ini mengikuti prinsip bahwa data dari luar sistem tidak pernah dipercaya sebelum dibuktikan.
const responsJson = '{"status":"ok","data":{"suhu":27,"lembap":61}}';
function parseJson(teks: string): unknown {
return JSON.parse(teks);
}
const payload: unknown = parseJson(responsJson);
console.log(payload);{ status: 'ok', data: { suhu: 27, lembap: 61 } }Fungsi parseJson sengaja mengembalikan unknown, meskipun JSON.parse sendiri mengembalikan any. Nilai yang tercetak sesuai dengan isi responsJson, tetapi compiler tidak mengizinkan satu pun akses properti terhadap payload. Percobaan akses langsung ditolak sebagaimana berkas berikut.
// tidak-dijalankan
const payload: unknown = JSON.parse('{"suhu":27}');
console.log(payload.suhu);# membuktikan pesan error untuk akses properti atas nilai unknown
tsc --noEmit --pretty false akses-mentah.tsakses-mentah.ts(4,13): error TS18046: 'payload' is of type 'unknown'.Pesan TS18046 muncul karena compiler menolak menebak bentuk nilai unknown. Penolakan ini justru menunjukkan langkah yang benar, yaitu mempersempit tipe lewat validasi sebelum bidangnya dibaca.
Type guard pada batas sistem#
Type guard adalah fungsi yang mengembalikan boolean dengan tanda tangan predikat, misalnya nilai is DataSuhu. Apabila seluruh pemeriksaan di dalamnya lolos, compiler memperlakukan nilai tersebut sebagai DataSuhu pada cabang if yang memanggilnya. Kontrak API fiktif sederhana ini memuat status bertipe string dan data berisi suhu serta lembap bertipe number.
type DataSuhu = {
status: string;
data: {
suhu: number;
lembap: number;
};
};
function adalahDataSuhu(nilai: unknown): nilai is DataSuhu {
if (typeof nilai !== "object" || nilai === null) {
return false;
}
const objek = nilai as Record<string, unknown>;
if (typeof objek.status !== "string" || typeof objek.data !== "object" || objek.data === null) {
return false;
}
const data = objek.data as Record<string, unknown>;
return typeof data.suhu === "number" && typeof data.lembap === "number";
}
const responsJson = '{"status":"ok","data":{"suhu":27,"lembap":61}}';
const payload: unknown = JSON.parse(responsJson);
if (adalahDataSuhu(payload)) {
console.log(`suhu: ${payload.data.suhu}`);
console.log(`lembap: ${payload.data.lembap}`);
} else {
console.log("payload tidak sesuai kontrak");
}
const responsRusak = '{"status":"ok","data":{}}';
const rusak: unknown = JSON.parse(responsRusak);
if (adalahDataSuhu(rusak)) {
console.log(rusak.data.suhu);
} else {
console.log("payload rusak ditolak");
}suhu: 27
lembap: 61
payload rusak ditolakGuard adalahDataSuhu memeriksa nilai sebagai objek yang bukan null, lalu memeriksa setiap bidang satu per satu hingga ke objek data yang bertingkat. Payload pertama lolos validasi, sehingga payload.data.suhu sah dan tercetak sebagai 27. Payload kedua kosong di bagian data, sehingga validasinya gagal dan program mencetak penolakan, bukan mencoba membaca bidang yang tidak ada. Penetapan tipe DataSuhu dengan demikian berdasarkan bukti pemeriksaan, bukan kepercayaan terhadap teks JSON. Satu guard yang ditulis teliti juga menjadi satu-satunya titik kepercayaan, sedangkan seluruh pemakaiannya bebas dari pemeriksaan manual.
Pola ini juga menjelaskan peran dua pemeriksaan kecil yang mudah dianggap berlebihan. Pemeriksaan nilai === null diperlukan karena typeof null menghasilkan string "object". Pemeriksaan typeof data.suhu === "number" diperlukan karena tipe bidang bertingkat baru diketahui compiler setelah validasi dilakukan.
Kesalahan umum#
Kesalahan pertama adalah menggunakan penegasan tipe (type assertion) seperti const data = (await respons.json()) as DataSuhu. Penegasan itu hanya meyakinkan compiler tanpa memberi bukti apa pun saat program berjalan, sehingga payload yang berbeda bentuk tetap lolos dan bidangnya bernilai undefined di tempat yang tidak terduga. Kesalahan semacam ini sulit terlihat pada data uji yang selalu benar, sehingga baru muncul ketika payload yang dikirim server berubah. Perbaikannya berupa type guard yang memeriksa setiap bidang sebelum nilai dipakai.
Kesalahan kedua adalah memeriksa typeof nilai === "object" tanpa menyingkirkan null. Karena typeof null menghasilkan "object", payload berupa null lolos pemeriksaan tersebut lalu gagal saat propertinya diakses. Urutan yang benar adalah menolak null lebih dahulu, sebagaimana baris nilai === null pada guard di atas.
Kesalahan ketiga adalah menganggap semua payload berupa objek. Payload berupa array tetap menghasilkan "object" pada typeof, sehingga pemeriksaannya memerlukan Array.isArray sebelum setiap elemennya divalidasi satu per satu. Tanpa pemeriksaan itu, kontrak yang sebenarnya berupa daftar dapat lolos sebagai objek biasa.
Ringkasan#
fetchmengembalikanPromise<Response>, sedangkanjson()mengembalikanPromise<any>yang bentuknya tidak dijamin compiler.- Payload di batas sistem diperlakukan sebagai
unknown, sehingga akses properti sebelum validasi ditolak dengan TS18046. - Type guard dengan predikat
nilai is DataSuhumempersempitunknownmenjadi tipe kontrak setelah setiap bidang diperiksa. - Validasi yang gagal diperlakukan sebagai jalur normal, sehingga payload rusak ditolak secara terkendali, bukan membuat program berhenti mendadak.
Referensi#
- fetch() — MDN Web Docs — Dokumentasi fetch beserta tipe kembaliannya.
- Response — MDN Web Docs — Properti status dan ok serta metode penguraian tubuh respons.
- JSON.parse() — MDN Web Docs — Penguraian teks JSON dan bentuk hasil yang beragam.
- Narrowing — The TypeScript Handbook — Pempersempit tipe dan predikat tipe untuk pemeriksaan bertahap.