Gimana Aku Bikin Type-Safe API Parsing di Proyek Gede TypeScript Tanpa Ngandelin Zod
Aku mau cerita sedikit nih, tentang salah satu tantangan paling "ngeselin" tapi juga paling vital dalam proyek-proyek besar berbasis TypeScript: parsing data API secara type-safe.
Kenapa ini penting? Karena di dunia nyata, API itu jahat. Data yang kamu terima dari backend (entah itu REST atau GraphQL) selalu dianggap unknown atau any oleh TypeScript, dan kalau kamu langsung pakai, tiba-tiba di runtime aplikasimu crash karena ternyata field yang seharusnya number malah isinya null atau string kosong. Horor, kan?
Banyak developer (termasuk aku dulu) langsung lari ke library validasi super populer seperti Zod, Valibot, atau Superstruct. Dan jujur, itu solusi yang brilian. Mereka bikin hidup gampang banget. Tapi tunggu dulu, bagaimana kalau kita lagi mengerjakan proyek yang sangat sensitif terhadap bundle size, atau kita ingin memegang kontrol penuh atas proses validasi tanpa menambah dependency eksternal?
Di proyek-proyek besar yang aku tangani di Javapixa Creative Studio, kami sering menghadapi kebutuhan untuk membuat parsing yang sangat spesifik dan efisien. Di sini, aku mau share gimana caranya aku bikin type-safe parsing yang solid hanya dengan mengandalkan fitur bawaan TypeScript (Type Guards dan Assertion Functions) dan sedikit kreativitas. Anggap saja ini adalah jalan ninja kita untuk menguasai TypeScript hingga ke akarnya.
*
1. Kenapa Kita Harus Keluar dari Zona Nyaman (Tinggalkan Zod Sementara)
Zod itu keren, Zod itu mudah. Tapi ada kalanya kamu harus belajar berenang tanpa pelampung. Ada tiga alasan utama kenapa pendekatan Type Guards murni ini patut dipertimbangkan, terutama di lingkungan profesional seperti yang kami terapkan di Javapixa:
- Zero External Dependency: Mengurangi dependency berarti mengurangi risiko keamanan, memudahkan maintenance, dan (seringkali) mengurangi ukuran bundle secara signifikan, yang sangat krusial untuk aplikasi berbasis client atau edge function.
- Full Control & Custom Error Handling: Ketika kamu membuat sendiri, kamu bisa mendefinisikan pesan kesalahan (error message) dan alur validasi persis seperti yang kamu mau, tanpa terikat struktur yang diberikan library.
- Mendalami Kekuatan TypeScript: Ini adalah cara terbaik untuk benar-benar mengerti bagaimana sistem tipe dan runtime berinteraksi.
Tujuan kita di sini bukan sekadar validasi data, tapi meyakinkan TypeScript di waktu kompilasi bahwa data yang kita pegang sudah sesuai dengan interface yang kita harapkan.
*
2. Pondasi Dasar: Runtime Type Guards (The is Keyword Magic)
Kalau kita ngomongin type safety di TypeScript, senjata utama kita adalah Type Guard. Ini adalah fungsi yang mengembalikan boolean dan menggunakan predikat tipe (data is SomeType) yang memberitahu compiler tentang tipe data tersebut jika fungsi mengembalikan true.
Kita mulai dari yang paling sederhana: membuat guard untuk tipe primitif.
typescript
// Guard untuk string
function isString(data: unknown): data is string {
return typeof data === 'string';
}Ini adalah building block kita. Tapi data API jarang sesederhana ini, kan?
3. Mengaplikasikan Assertion Functions untuk Alur yang Lebih Bersih
Saat kita mem-parsing, kita tidak mau kode kita penuh dengan if (isString(data)) { ... } else { throw error }. Kita ingin alur kode kita lebih bersih dan memaksa program berhenti jika validasi gagal. Di sinilah Assertion Functions (dengan kata kunci asserts) masuk.
Assertion Function ini pada dasarnya adalah Type Guard yang tidak mengembalikan boolean, melainkan melempar error jika kondisinya tidak terpenuhi. Jika ia berhasil melewati fungsi tersebut, TypeScript akan otomatis menganggap tipe data itu sudah safe.
Misalnya, kita buat fungsi untuk memastikan bahwa data itu adalah objek yang tidak null:
typescript
class ParsingError extends Error {
constructor(message: string, public path: string = '') {
super(Parsing Error at ${path}: ${message});
this.name = 'ParsingError';
}
}Keren, kan? Sekarang kita bisa mulai menyusun validasi untuk objek kompleks.
*
4. Mengurai Objek Kompleks (The Real Pain)
Anggaplah kita punya interface data pengguna yang kompleks dari API:
typescript
interface UserData {
id: number;
username: string;
isActive: boolean;
roles: string[];
lastLogin?: number | null; // Optional dan bisa null
}Tugas kita sekarang adalah membuat isUserData(data: unknown): data is UserData.
Langkah 4.1: Memastikan Data Adalah Objek Dasar
Kita panggil dulu fungsi assertObject yang sudah kita buat tadi:
typescript
function assertUserData(data: unknown, path: string = 'root'): asserts data is UserData {
// 1. Pastikan data adalah objek non-null
assertObject(data, path);
const obj = data as any; // Kita cast ke any sementara untuk memudahkan akses property// 2. Cek semua properti wajib (Required Properties)
// Validasi 'id'
const idPath = ${path}.id;
if (!isSafeNumber(obj.id)) {
throw new ParsingError('Field is missing or not a safe number', idPath);
}
// Validasi 'username'
const usernamePath = ${path}.username;
if (!isString(obj.username)) {
throw new ParsingError('Field is missing or not a string', usernamePath);
}Langkah 4.2: Menangani Array dan Struktur Bersarang
Bagaimana dengan roles: string[]? Kita harus memastikan bahwa roles adalah sebuah array, dan setiap elemen di dalamnya adalah string.
Kita perlu membuat guard array yang generik:
typescript
// Ini adalah Type Guard generik untuk Array
function assertArrayOf(
data: unknown,
path: string,
assertItem: (item: unknown, itemPath: string) => asserts item is T
): asserts data is T[] {
if (!Array.isArray(data)) {
throw new ParsingError('Expected an array', path);
}
// Kita cek setiap item dalam array
data.forEach((item, index) => {
const itemPath = ${path}[${index}];
// Panggil assertion function untuk setiap elemen
try {
assertItem(item, itemPath);
} catch (e) {
// Kita harus melempar error yang lebih spesifik jika validasi item gagal
if (e instanceof ParsingError) {
// re-throw error dengan path yang sudah diperbarui
throw new ParsingError(e.message, itemPath);
}
throw e;
}
});
}Sekarang kita bisa menggunakan fungsi ini di dalam assertUserData untuk properti roles:
typescript
// Lanjutan dari assertUserData...// Validasi 'roles'
const rolesPath = ${path}.roles;
// Kita butuh Assertion Function sederhana untuk memastikan item adalah string
const assertString = (item: unknown, itemPath: string): asserts item is string => {
if (!isString(item)) {
throw new ParsingError('Expected string inside array', itemPath);
}
};Langkah 4.3: Mengelola Properti Opsional (Optional dan Nullable)
Properti lastLogin?: number | null adalah yang paling tricky. Ada tiga skenario valid:
- Properti tidak ada sama sekali (
undefined). - Properti ada dan nilainya
null. - Properti ada dan nilainya
number.
Kita hanya perlu melakukan validasi jika properti itu ada dan nilainya bukan null.
typescript
// Lanjutan dari assertUserData...// Validasi 'lastLogin' (opsional dan nullable)
const lastLoginPath = ${path}.lastLogin;// Cek apakah key 'lastLogin' ada di objek (menggunakan 'in' operator)
if ('lastLogin' in obj) {
const value = obj.lastLogin;Dengan mengombinasikan Type Guards primitif, Assertion Functions, dan logika pengecekan properti ('prop' in obj), kita telah menciptakan parser data yang 100% type-safe secara runtime dan compile-time tanpa satu pun dependency eksternal.
*
5. Mengapa Pendekatan Ini Krusial di Proyek Skala Besar
Ketika kita dihadapkan pada proyek yang skalanya masif—misalnya, sistem e-commerce dengan ratusan endpoint atau aplikasi manajemen data yang sensitif—pendekatan yang ketat ini menjadi investasi yang sangat berharga.
Di Javapixa Creative Studio, kami selalu menekankan bahwa kode haruslah predictable.
- Debugging Menjadi Jauh Lebih Cepat: Ketika parsing gagal, kita tahu persis di mana letak masalahnya (berkat
pathyang kita inject ke dalamParsingError), misalnya:Parsing Error at root.user.address[2].zipCode: Expected string. Ini jauh lebih baik daripada sekadarCannot read properties of undefined. - Modularitas dan Reusabilitas: Type Guards yang sudah kita buat (misalnya
assertArrayOfatauassertObject) bisa digunakan ulang di mana saja dalam aplikasi tanpa perlu mendefinisikan skema ulang. Ini menciptakan ekosistem tipe yang konsisten di seluruh codebase. - Performansi: Karena kita hanya menggunakan fungsi native JavaScript (
typeof,Array.isArray,Number.isFinite), performanya sangat cepat, seringkali lebih unggul dari library validasi yang mungkin membawa overhead pemrosesan skema mereka sendiri.
Tentu, proses awal membuat Type Guard untuk 10 interface pertama akan terasa lebih lama daripada menggunakan Zod. Tapi begitu kita punya library internal Type Guards yang solid, kecepatan pengembangan akan meningkat drastis, dan yang paling penting, stabilitas aplikasi kita menjadi terjamin.
Ini adalah standar kehati-hatian teknis yang kami pegang teguh saat merancang arsitektur aplikasi di Javapixa Creative Studio. Kami percaya bahwa fondasi aplikasi yang kuat harus dibangun di atas type safety yang ketat, memastikan bahwa aplikasi yang kami deliver kepada klien adalah solusi yang elegan, performatif, dan minim bug di runtime.
*
Penutup: Kapan Beralih Kembali ke Zod?
Setelah membahas semua kerumitan ini, kita harus realistis. Kapan Type Guards murni ini terlalu berlebihan?
Jika proyekmu kecil, atau deadline sangat mepet, atau kamu butuh skema validasi yang sangat kompleks (misalnya validasi silang antar field atau validasi berbasis database yang harus dilakukan secara schema definition), Zod tetaplah pilihan yang sangat praktis.
Namun, jika kamu sedang membangun fondasi sebuah framework internal, library yang akan dipakai oleh banyak tim, atau aplikasi yang sangat butuh performance dan kontrol total atas bundle size, meluangkan waktu untuk mengimplementasikan type safety menggunakan Type Guards dan Assertion Functions adalah investasi yang sangat layak. Kamu tidak hanya membuat kode yang lebih baik, tapi juga benar-benar menguasai bahasa TypeScript itu sendiri. Selamat mencoba!