Ikon Kartu dari Union Type, Dijaga Kompiler
Registry as const plus union type membuat kompiler TypeScript menjaga konsistensi ikon kartu perusahaan di tiga tempat sekaligus.
Ringkasan
Awalnya tujuh kartu perusahaan pakai ikon gedung yang sama, jadinya seragam dan gitu-gitu aja. Solusinya, pilihan ikon dipindah ke data konten pakai union type plus registry kecil, jadi tiap bisnis punya ikon sendiri. Kerennya, kompiler TypeScript yang jagain konsistensi—salah ketik atau lupa satu file dua bahasa langsung gagal build, bukan kelihatan aneh pas production.
Pertama kali saya perhatikan bagian jaringan perusahaan di halaman utama, ada satu ikon Building2 yang dipakai untuk ketujuh kartu. Perusahaan pengamanan, holding, properti, teknologi, konsultansi, konstruksi, semuanya bergedung seragam dengan glyph gedung yang sama. Satu commit bahkan menghapusnya sama sekali, lalu commit sesudahnya mengembalikan glyph telanjang di atas nama tanpa kartu dan tanpa latar. Dua langkah itu masuk akal sebagai reaksi: kartu yang seragam semua terasa seperti dekorasi tempelan. Tapi jalur keluarnya ternyata bukan memilih antara ada atau tidaknya ikon, melainkan membedakannya per bisnis lewat data, dan yang menjaga semuanya tetap sinkron bukan disiplin manual melainkan kompiler TypeScript.
Nambah ikon di JSX itu jalan buntu
Jalan pikiran paling natural waktu itu sederhana. Kalau tiap perusahaan butuh ikon sendiri, ya tinggal tempel komponen ikonnya langsung di JSX masing-masing kartu. Praktis untuk tujuh kartu, tapi polanya rapuh. Pilihan ikon tinggal di komponen, sementara data perusahaannya ada di modul konten yang punya tipe ketat. Tiga tempat harus senada terus: data konten, tipe data, dan render. Lupa satu saja, yang muncul bukan error, melainkan kartu yang diam-diam tampil tanpa ikon atau semua kartu kembali seragam.
Ada lapisan masalah kedua yang kurang terlihat: data konten proyek ini diduplikasi dua bahasa. Artinya keputusan ikon per perusahaan harus dicatat di file konten bahasa Indonesia dan bahasa Inggris sekaligus. Kalau penjagaannya berupa kesepakatan lisan di antara developer, hampir pasti ada file yang tertinggal, dan yang aneh, halamannya tetap tampil tanpa keluhan apa pun. Yang butuh di sini bukan hati-hati ekstra, tapi mekanisme yang menolak perubahan setengah jalan.
Registry tertutup, bukan string liar
Solusinya: pindahkan seluruh pilihan ikon ke modul konten, lalu kunci kemungkinannya lewat tipe. Komponen cuma menyimpan satu registry kecil:
const ICONS = {
shield: Shield,
building: Building2,
home: Home,
"land-plot": LandPlot,
cpu: Cpu,
briefcase: Briefcase,
"hard-hat": HardHat,
} as const;
Dengan as const, setiap kunci di ICONS punya tipe literal, bukan tipe string umum. Di berkas content/types.ts, kartu perusahaan mendapat field wajib bertipe union tertutup [1]. Bentuknya:
icon:
| "shield"
| "building"
| "home"
| "land-plot"
| "cpu"
| "briefcase"
| "hard-hat";
Render-nya tinggal satu baris, const Icon = ICONS[company.icon]. Ini indexed access: tipe hasil pencarian diambil dari properti yang dirujuk, dan mengakses properti yang tidak ada langsung jadi error kompilasi, bukan nilai kosong di production [2].
Bukti kerjanya kelihatan waktu dua perusahaan properti perlu dibedakan. Satu commit yang sama menambah anggota union land-plot, entri registry-nya, dan field ikon di data dua bahasa sekaligus. Tiga file berubah selaras dalam satu push. Seandainya salah satunya tertinggal, build gagal sebelum sempat deploy. Bandingkan dengan versi manual: satu commit lupa satu file konten, dan tidak ada yang sadar sampai berbulan-bulan.
Tiga jaminan yang aktif seketika
Setelah struktur ini berdiri, kompiler jadi penjaga dengan tiga aturan main. Pertama, kunci di data konten harus anggota union, jadi salah ketik seperti menulis landplot tanpa tanda hubung ditolak saat kompilasi, bukan saat kelihatan aneh di halaman. Kedua, anggota union wajib punya entri di registry, jadi menghapus import HardHat sambil union masih menyebut "hard-hat" pasti kena panggil. Ketiga, field ikonnya wajib diisi, sehingga tidak ada lagi kartu yang lahir tanpa identitas visual.
Pendekatan ini juga murah. Komponen Lucide adalah komponen standalone yang fully typed dan tree-shakable, hanya ikon yang diimport yang ikut ke bundle [3]. Saya pribadi nggak tertarik menggantinya dengan dynamic import untuk sekian ikon statis, itu overkill. Map string biasa juga kurang: terlalu longgar, konsistensinya bergantung pada kebiasaan baik semua orang yang menyentuh file. Union type memberi validasi tanpa biaya runtime, dan untuk data UI yang himpunannya memang tertutup, itu tarif yang pantas dibayar.
Sumber
[1] TypeScript Handbook — Everyday Types (union types)