kodingup.camp

Beranda/TypeScript Mastery

Tipe untuk Data API

Modul 11 dari 125 menit bacaTyping API Data

Isi modul

    Kenapa ini penting

    Semua yang kamu pelajari sejauh ini bekerja di compile time. Masalahnya, data paling berbahaya di aplikasimu datang dari luar dan baru ada saat runtime: respons API, isi form, query string URL, localStorage, webhook dari payment gateway. Kalau kamu menulis const user = (await res.json()) as User, kamu sedang berbohong kecil kepada TypeScript. Compiler percaya, editor memberi autocomplete, tapi kalau server ternyata mengirim harga sebagai string atau nama bernilai null, aplikasimu tetap meledak, kali ini dengan rasa aman yang palsu.

    Analoginya seperti satpam yang hanya membaca daftar tamu di kertas tanpa pernah mengecek KTP orang yang datang. Daftar tamunya rapi (tipe), tapi siapa pun yang mengaku bernama "Budi" bisa masuk. Validasi runtime adalah pengecekan KTP itu. Di modul ini kamu menggabungkan keduanya dengan Zod (versi 4.6.7), library yang sudah kamu kenal di materi Library: satu skema yang memvalidasi data saat runtime sekaligus menghasilkan tipe TypeScript, sehingga keduanya tidak pernah tidak sinkron.

    Konsep inti

    Kenapa res.json() bertipe Promise<any>

    Response.json() di tipe bawaan DOM mengembalikan Promise<any>. Artinya hasil fetch langsung "menular" any ke seluruh kodemu kalau tidak hati-hati. Ada tiga tingkat keamanan:

    Cara Contoh Keamanan
    Biarkan any const data = await res.json() Tidak ada pemeriksaan sama sekali
    Assertion const data = (await res.json()) as User[] Autocomplete ada, tapi hanya janji
    Validasi runtime const data = SkemaUser.array().parse(await res.json()) Diperiksa sungguhan, tipe dijamin

    Untuk prototipe cepat, assertion kadang cukup. Untuk aplikasi yang dipakai orang lain, validasi di batas aplikasi (tempat data masuk) adalah praktik standar.

    Skema Zod sebagai satu sumber kebenaran

    import { z } from "zod";
    
    const SkemaProduk = z.object({
      id: z.number().int().positive(),
      nama: z.string().min(1),
      harga: z.number().nonnegative(),
      kategori: z.enum(["makanan", "minuman"]),
      gambar: z.url().optional(),
    });
    
    type Produk = z.infer<typeof SkemaProduk>;
    // { id: number; nama: string; harga: number; kategori: "makanan" | "minuman"; gambar?: string | undefined }
    

    z.infer<typeof Skema> menurunkan tipe TypeScript dari skema. Kamu tidak menulis type Produk secara manual. Tambah satu field di skema, tipenya ikut berubah. Di Zod 4, validator format seperti z.email(), z.url(), dan z.uuid() tersedia langsung di z (bentuk lama z.string().email() masih ada tetapi ditandai deprecated).

    parse vs safeParse

    Perhatikan bahwa hasil safeParse adalah discriminated union, persis pola yang kamu pelajari di modul Union & Narrowing. Setelah if (!hasil.success), TypeScript tahu hasil.data tersedia.

    z.input vs z.output dan transformasi

    Skema bisa mengubah data saat validasi, misalnya string tanggal menjadi Date atau string angka dari query URL menjadi number:

    import { z } from "zod";
    
    const SkemaQuery = z.object({
      halaman: z.coerce.number().int().min(1).default(1),
    });
    

    Dalam kasus seperti ini tipe "sebelum" dan "sesudah" berbeda. z.input<typeof S> adalah bentuk sebelum diproses, z.output<typeof S> (sama dengan z.infer) adalah bentuk setelahnya.

    Di mana validasi dilakukan

    Validasi sekali, di pintu masuk:

    Server API ──► fetchJson(url, Skema) ──► data bertipe & tervalidasi ──► komponen React
                         ▲
              di sini satu-satunya tempat data "liar" disentuh
    

    Komponen tidak perlu tahu soal validasi; mereka menerima data yang sudah pasti benar. Pola ini juga berlaku untuk localStorage, searchParams, dan FormData.

    Membuat function fetch generik

    Gabungkan generics dan Zod: function yang menerima URL dan skema, lalu mengembalikan tipe yang diturunkan dari skema.

    import { z } from "zod";
    
    async function ambilJson<S extends z.ZodType>(url: string, skema: S): Promise<z.infer<S>> {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return skema.parse(await res.json());
    }
    

    Pemanggil cukup menulis ambilJson(url, SkemaProduk.array()) dan hasilnya otomatis bertipe Produk[], tanpa as sama sekali.

    Kapan tipe saja sudah cukup

    Validasi runtime punya biaya (kode tambahan, sedikit waktu proses). Kamu tidak perlu memvalidasi data yang sepenuhnya dikendalikan kodemu sendiri, seperti object konstanta atau data antar-komponen. Validasi wajib untuk data yang melewati batas kepercayaan: jaringan, input user, penyimpanan browser, dan layanan pihak ketiga. Di Next.js nanti, hal yang sama berlaku untuk body request di Route Handler dan Server Action.

    Contoh kode

    // api.ts, fetch bertipe + validasi runtime dengan Zod 4
    import { z } from "zod";
    
    // 1. Skema = satu sumber kebenaran
    const SkemaTodo = z.object({
      userId: z.number(),
      id: z.number(),
      title: z.string(),
      completed: z.boolean(),
    });
    export type Todo = z.infer<typeof SkemaTodo>;
    
    // 2. Error kustom supaya pemanggil bisa membedakan jenis kegagalan
    export class ApiError extends Error {
      readonly status: number;
      constructor(status: number, pesan: string) {
        super(pesan);
        this.name = "ApiError";
        this.status = status;
      }
    }
    
    // 3. Function fetch generik: tipe hasil diturunkan dari skema
    export async function ambilJson<S extends z.ZodType>(url: string, skema: S): Promise<z.infer<S>> {
      const res = await fetch(url);
      if (!res.ok) throw new ApiError(res.status, `Gagal memuat ${url}`);
      const mentah: unknown = await res.json();
      const hasil = skema.safeParse(mentah);
      if (!hasil.success) {
        throw new Error(`Data dari server tidak sesuai:\n${z.prettifyError(hasil.error)}`);
      }
      return hasil.data;
    }
    
    // 4. Pemakaian: tanpa 'as', tipe todos otomatis Todo[]
    const todos = await ambilJson("https://jsonplaceholder.typicode.com/todos?_limit=3", z.array(SkemaTodo));
    console.log(todos.map((t) => `${t.completed ? "✅" : "⬜"} ${t.title}`).join("\n"));
    // → ⬜ delectus aut autem
    // → ⬜ quis ut nam facilis et officia qui
    // → ⬜ fugiat veniam minus
    
    console.log(todos[0]?.judul); // ❌ error TS2339: Property 'judul' does not exist on type '{ userId: number; id: number; title: string; completed: boolean; }'.
    

    Sekarang lihat apa yang terjadi saat server mengirim data yang tidak sesuai. Ini persis kasus yang tidak bisa ditangkap TypeScript sendirian:

    // validasi.ts, data "liar" dari server
    import { z } from "zod";
    
    const SkemaProduk = z.object({
      id: z.number(),
      nama: z.string().min(1, "Nama produk wajib diisi"),
      harga: z.number().nonnegative("Harga tidak boleh negatif"),
      dibuatPada: z.iso.datetime().transform((s) => new Date(s)), // string ISO → Date
    });
    type ProdukMasuk = z.input<typeof SkemaProduk>; // dibuatPada: string
    type Produk = z.output<typeof SkemaProduk>;     // dibuatPada: Date
    
    // Anggap ini respons server yang ternyata rusak
    const responsServer: unknown = {
      id: 1,
      nama: "",
      harga: "15000",
      dibuatPada: "2026-10-24T08:00:00Z",
    };
    
    // Cara berbahaya: assertion. Tidak ada error compile, tapi data salah.
    const palsu = responsServer as Produk;
    console.log(typeof palsu.harga); // → string   (padahal tipenya number!)
    
    // Cara aman: safeParse
    const hasil = SkemaProduk.safeParse(responsServer);
    if (!hasil.success) {
      console.log(z.prettifyError(hasil.error));
      // → ✖ Nama produk wajib diisi
      // →   → at nama
      // → ✖ Invalid input: expected number, received string
      // →   → at harga
    } else {
      const produk: Produk = hasil.data;
      console.log(produk.dibuatPada.getFullYear());
    }
    
    // Data yang benar lolos dan sudah ditransformasi
    const valid = SkemaProduk.parse({ id: 2, nama: "Teh", harga: 5000, dibuatPada: "2026-10-24T08:00:00Z" });
    console.log(valid.dibuatPada instanceof Date, valid.dibuatPada.getUTCFullYear()); // → true 2026
    
    const contohMasuk: ProdukMasuk = { id: 3, nama: "Kopi", harga: 8000, dibuatPada: "2026-10-24T08:00:00Z" };
    console.log(SkemaProduk.parse(contohMasuk).nama); // → Kopi
    

    Terakhir, memakai fungsi fetch tervalidasi di komponen React. Contoh di bawah memakai useEffect supaya mandiri; catatan di dalamnya menunjukkan versi TanStack Query yang lebih ringkas:

    // DaftarTodo.tsx, data tervalidasi masuk ke komponen
    import { useEffect, useState } from "react";
    import { z } from "zod";
    
    const SkemaTodo = z.object({ id: z.number(), title: z.string(), completed: z.boolean() });
    type Todo = z.infer<typeof SkemaTodo>;
    
    async function ambilTodos(): Promise<Todo[]> {
      const res = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=5");
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return z.array(SkemaTodo).parse(await res.json());
    }
    
    // Dengan TanStack Query cukup: useQuery({ queryKey: ["todos"], queryFn: ambilTodos })
    // dan 'data' otomatis bertipe Todo[] | undefined. Versi tanpa library:
    export function DaftarTodo() {
      const [todos, setTodos] = useState<Todo[]>([]);
      const [error, setError] = useState<string | null>(null);
    
      useEffect(() => {
        ambilTodos()
          .then(setTodos)
          .catch((e: unknown) => setError(e instanceof Error ? e.message : "Gagal memuat"));
      }, []);
    
      if (error) return <p role="alert">Error: {error}</p>;
      return (
        <ul>
          {todos.map((t) => (
            <li key={t.id}>{t.completed ? "✅" : "⬜"} {t.title}</li>
          ))}
        </ul>
      );
    }
    

    Kesalahan umum

    1. Menganggap as User sebagai validasi. Assertion hanya membungkam compiler. Kalau data salah, bug tetap terjadi, dan sekarang lebih sulit dilacak karena tipenya "terlihat benar".
    2. Menulis tipe dan skema secara terpisah. type Produk = {...} plus z.object({...}) yang ditulis manual pasti suatu hari tidak sinkron. Turunkan tipe dengan z.infer.
    3. Memvalidasi di setiap komponen. Validasi sekali di pintu masuk (function fetch, loader, handler). Komponen menerima data yang sudah pasti benar.
    4. Memakai parse tanpa menangani error. parse melempar ZodError. Bungkus dengan try/catch, pakai safeParse, atau biarkan TanStack Query menangkapnya sebagai error.
    5. Lupa bahwa input dan output skema bisa berbeda. Dengan transform, coerce, atau default, bentuk sebelum dan sesudah validasi berbeda. Pakai z.input untuk data mentah (misalnya default values form) dan z.output/z.infer untuk hasilnya.

    Latihan

    1. Mudah, Buat skema Zod SkemaPengguna dengan id (number), nama (string minimal 2 karakter), email (z.email()), dan aktif (boolean). Turunkan tipe Pengguna dengan z.infer.
    2. Sedang, Tulis function bacaPengaturan(): Pengaturan yang membaca JSON dari localStorage dengan kunci "pengaturan", memvalidasi dengan skema { tema: "terang" | "gelap", ukuranFont: number 12-24 }, dan mengembalikan nilai default kalau data tidak ada atau tidak valid.
    3. Menantang, Buat skema respons berhalaman generik: function skemaHalaman<T extends z.ZodType>(item: T) yang mengembalikan z.object({ items: z.array(item), halaman: z.number(), total: z.number() }). Pakai untuk memvalidasi daftar produk dan turunkan tipenya.
    Lihat solusi
    import { z } from "zod";
    
    // 1. Mudah
    const SkemaPengguna = z.object({
      id: z.number(),
      nama: z.string().min(2),
      email: z.email(),
      aktif: z.boolean(),
    });
    type Pengguna = z.infer<typeof SkemaPengguna>;
    const p: Pengguna = SkemaPengguna.parse({ id: 1, nama: "Sari", email: "sari@contoh.id", aktif: true });
    console.log(p.email); // → sari@contoh.id
    
    // 2. Sedang
    const SkemaPengaturan = z.object({
      tema: z.enum(["terang", "gelap"]),
      ukuranFont: z.number().int().min(12).max(24),
    });
    type Pengaturan = z.infer<typeof SkemaPengaturan>;
    const DEFAULT_PENGATURAN: Pengaturan = { tema: "terang", ukuranFont: 16 };
    
    function bacaPengaturan(): Pengaturan {
      try {
        const teks = localStorage.getItem("pengaturan");
        if (teks === null) return DEFAULT_PENGATURAN;
        const hasil = SkemaPengaturan.safeParse(JSON.parse(teks));
        return hasil.success ? hasil.data : DEFAULT_PENGATURAN;
      } catch {
        return DEFAULT_PENGATURAN; // JSON rusak atau localStorage diblokir
      }
    }
    console.log(typeof bacaPengaturan);
    
    // 3. Menantang
    function skemaHalaman<T extends z.ZodType>(item: T) {
      return z.object({
        items: z.array(item),
        halaman: z.number().int().min(1),
        total: z.number().int().nonnegative(),
      });
    }
    
    const SkemaProduk = z.object({ id: z.number(), nama: z.string(), harga: z.number() });
    const SkemaHalamanProduk = skemaHalaman(SkemaProduk);
    type HalamanProduk = z.infer<typeof SkemaHalamanProduk>;
    // { items: { id: number; nama: string; harga: number }[]; halaman: number; total: number }
    
    const respons: HalamanProduk = SkemaHalamanProduk.parse({
      items: [{ id: 1, nama: "Buku", harga: 45000 }],
      halaman: 1,
      total: 1,
    });
    console.log(respons.items[0]?.nama); // → Buku
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Strengthen Your Fetch API Calls with ZodTypeScript with Benny Code · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.