Tipe TypeScript Nggak Ikut ke JSON, Makanya Ada Kontrak Test
Interface TypeScript ilang saat runtime, jadi JSON konten butuh contract test. Cerita dari commit egperkasa-website: optional property, type erasure, dan it.each Vitest.
Pas gue merge konten compro 2026 ke repo egperkasa-website, terminal nongol error TS2741: Property 'image' is missing in type. Satu sertifikasi ISO 45001:2018 yang gue tambahin nggak punya gambar badge, dan interface Certification di src/types/site-data.ts mewajibkan image: string. Gue sempet bengong, soalnya data lainnya aman semua.
Data situs itu hidup di JSON per locale: frontend/src/data/id/site-data.json dan en/site-data.json. Dibaca pakai getSiteData(locale), terus di-cross-check sama interface TypeScript. OHSAS 18001 gue buang, ISO 45001 masuk. Cabang Serang nambah. Blok welfare buat koperasi sama CSR juga baru. Gue sempet mikir ini cuma masalah data aja, tinggal isi string kosong, beres. Tapi pas ngecek ke tim desain, badge ISO 45001 emang belum jadi. Nunggu seminggu buat sekadar lolos compile rasanya nggak masuk akal.
Tanda tanya itu keputusan skema data
Jadi gue ngubah image: string jadi image?: string di interface. Satu karakter tanda tanya. Di TypeScript, optional properties itu legit: property boleh nggak ada sama sekali. Gue juga nambahin struktur welfare yang baru ke AboutData.
export interface Certification {
name: string;
image?: string;
}
export interface WelfareBlock {
title: string;
description: string;
items: string[];
}
export interface WelfareData {
koperasi: WelfareBlock;
csr: WelfareBlock;
}
Tapi di situlah gue sadar ada yang aneh dari cara gue mikir sebelumnya. Interface itu ilang total pas runtime. Beneran ilang. TypeScript itu compile-time only, tipe yang gue tulis terhapus pas kompilasi dan nggak pernah ada di JavaScript yang jalan di browser. FAQ resmi TS bilang type erasure berarti nggak ada runtime check. JSON yang gue import ke getSiteData nggak pernah divalidasi bentuknya oleh TypeScript di production. Waktu gue nambahin welfare block, pas coreServices jadi 8 item, atau cabang Serang masuk, nggak ada satupun yang dijaga sama tipe saat aplikasi jalan.
Tanda tanya di image? bukan cuma anotasi buat compiler. Itu keputusan skema data. Gue mutusin: sertifikasi boleh nggak punya badge. Dan karena JSON-nya diedit manusia tiap ada update konten, gue butuh jaminan bentuknya bener tanpa ngandelin tipe yang bakal hilang.
Contract test yang jalan buat dua locale
Makanya gue nulis contract test. Pakai vitest, parametrized dengan it.each([id, en]) biar assertion sama jalan buat dua locale sekaligus. Penjelasan it.each ada di dokumentasi Vitest.
const locales: Locale[] = ["id", "en"];
it.each(locales)("Certification accepts missing image (%s)", (locale) => {
const cert: Certification = { name: "ISO 45001:2018" };
expect(cert.image).toBeUndefined();
});
it.each(locales)("welfare block present with non-empty items (%s)", (locale) => {
const { about } = getSiteData(locale);
expect(about.welfare.koperasi.items.length).toBeGreaterThan(0);
expect(about.welfare.csr.items.length).toBeGreaterThan(0);
});
Di test itu gue pastiin welfare block ada dan items nggak kosong. coreServices harus pas 8, kantor Serang wajib ada di list, values pas 4 dan yang pertama Amanah. Terus gue cek sertifikasi ada yang 45001 dan nggak ada OHSAS, detailSections memuat PoC sama SIAPAkses. Yang paling penting buat kasus badge: expect(cert.image).toBeUndefined() buat sertifikasi tanpa gambar.
Gue pribadi lebih milih contract test begini daripada nempel validator runtime kayak Zod atau AJV buat data konten. Kalo manusia yang edit JSON tiap bulan, test yang baca file langsung dan failed pas CI itu umpan baliknya lebih kenceng daripada schema validation yang cuma jalan pas runtime nanti. Gue juga pernah nulis soal bug yang lolos gara-gara test verify 0 fail dan soal kontrak di code block Shiki. Polanya sama: jangan percaya bentuk data cuma karena tipe di atas kertas.
Keputusan akhir gue: image? dibiarin optional, contract test jadi gerbang utama tiap ada merge konten baru. TypeScript jaga gue pas nulis kode, tapi JSON-nya sendiri butuh saksi mata.