kodingup.camp

Beranda/TypeScript Mastery

TypeScript di React

Modul 10 dari 125 menit bacaTypeScript with React

Isi modul

    Kenapa ini penting

    Di React, bug yang paling sering muncul berkaitan dengan props: lupa mengirim props wajib, salah nama props, mengirim string padahal komponen butuh angka, atau memanggil onSelect(id) padahal komponen induk mengharapkan object. Tanpa TypeScript, kesalahan ini baru ketahuan saat halaman dibuka. Dengan TypeScript, setiap komponen menjadi kontrak yang jelas, dan editor menunjukkan props apa saja yang tersedia begitu kamu mengetik <Tombol.

    Bayangkan komponen React seperti colokan listrik. Colokan tiga kaki tidak bisa dimasukkan ke stopkontak dua lubang, dan kamu langsung tahu sebelum menyalakan apa pun. Tipe props adalah bentuk colokan itu. Hampir semua proyek React profesional (termasuk Next.js yang akan kamu pelajari berikutnya) memakai file .tsx, jadi pola-pola di modul ini akan kamu pakai setiap hari.

    Konsep inti

    File .tsx dan tipe React

    File yang berisi JSX memakai ekstensi .tsx. Tipe React disediakan oleh paket @types/react dan @types/react-dom (versi 19.3 saat materi ini ditulis), yang sudah terpasang di template Vite react-ts. Kamu bisa mengimpor tipe dengan import type { ReactNode } from "react" atau memakai namespace React.ReactNode setelah import React from "react". Di materi ini kita memakai import type.

    Props type

    type KartuProdukProps = {
      nama: string;
      harga: number;
      diskon?: number;               // opsional
      onBeli: (nama: string) => void; // callback
    };
    
    function KartuProduk({ nama, harga, diskon = 0, onBeli }: KartuProdukProps) {
      return <button onClick={() => onBeli(nama)}>{nama} - Rp{harga - diskon}</button>;
    }
    

    Tidak perlu menulis return type komponen; TypeScript menyimpulkannya. Hindari React.FC untuk komponen baru: ia tidak memberi manfaat tambahan dan dulu menambahkan children secara diam-diam. Function biasa dengan props bertipe sudah cukup.

    children

    children adalah props biasa. Tipe yang paling umum: ReactNode, yang mencakup elemen JSX, string, angka, null, array, dan lain-lain.

    import type { ReactNode } from "react";
    
    type KotakProps = { judul: string; children: ReactNode };
    

    Ada juga PropsWithChildren<P> yang menambahkan children?: ReactNode ke props P.

    Mewarisi props elemen HTML

    Untuk komponen yang membungkus elemen bawaan (tombol, input), jangan tulis ulang onClick, disabled, type, dan puluhan atribut lain. Pakai ComponentProps<"button">:

    import type { ComponentProps } from "react";
    
    type TombolProps = ComponentProps<"button"> & { varian?: "utama" | "bahaya" };
    

    Tipe event

    Event Tipe di @types/react 19.3 Contoh
    Klik MouseEvent<HTMLButtonElement> onClick
    Ketik di input ChangeEvent<HTMLInputElement> onChange pada <input>
    Pilih di select ChangeEvent<HTMLSelectElement> onChange pada <select>
    Submit form SubmitEvent<HTMLFormElement> onSubmit
    Tombol keyboard KeyboardEvent<HTMLInputElement> onKeyDown
    Fokus FocusEvent<HTMLInputElement> onBlur

    Catatan versi: di @types/react 19.3, tipe FormEvent sudah ditandai deprecated dengan keterangan "FormEvent doesn't actually exist". Untuk onSubmit, pakai SubmitEvent<HTMLFormElement>. Tutorial lama yang memakai React.FormEvent masih jalan, tapi editor akan mencoretnya.

    Trik termudah: kalau handler ditulis langsung di JSX (onChange={(e) => ...}), kamu tidak perlu menulis tipe event sama sekali karena sudah disimpulkan. Arahkan kursor ke e di editor untuk melihat tipenya, lalu salin kalau handler perlu dipindah ke function terpisah.

    Event di sini adalah tipe milik React (diimpor dari "react"), bukan MouseEvent bawaan DOM. Kalau kamu lupa mengimpornya, TypeScript akan memakai MouseEvent global milik DOM dan muncul error yang membingungkan.

    Hooks dengan tipe

    Generic component

    Komponen juga bisa generic, misalnya daftar yang bisa menampilkan item apa saja:

    import type { ReactNode } from "react";
    
    type DaftarProps<T> = {
      items: T[];
      render: (item: T) => ReactNode;
      ambilKey: (item: T) => string | number;
    };
    
    function Daftar<T>({ items, render, ambilKey }: DaftarProps<T>) {
      return <ul>{items.map((item) => <li key={ambilKey(item)}>{render(item)}</li>)}</ul>;
    }
    

    Saat dipakai dengan items={produk}, TypeScript tahu item di render adalah Produk.

    Contoh kode

    // Komponen.tsx, props, children, event, dan props HTML
    import { useState } from "react";
    import type { ChangeEvent, ComponentProps, ReactNode, SubmitEvent } from "react";
    
    // 1. Membungkus <button> tanpa menulis ulang atributnya
    type TombolProps = ComponentProps<"button"> & {
      varian?: "utama" | "bahaya";
    };
    
    export function Tombol({ varian = "utama", className = "", ...sisa }: TombolProps) {
      const warna = varian === "utama" ? "bg-blue-600" : "bg-red-600";
      return <button className={`${warna} rounded px-4 py-2 text-white ${className}`} {...sisa} />;
    }
    
    // 2. children
    type KartuProps = { judul: string; children: ReactNode };
    
    export function Kartu({ judul, children }: KartuProps) {
      return (
        <section className="rounded border p-4">
          <h2 className="font-bold">{judul}</h2>
          {children}
        </section>
      );
    }
    
    // 3. Form dengan event bertipe
    type FormLoginProps = { onLogin: (email: string) => void };
    
    export function FormLogin({ onLogin }: FormLoginProps) {
      const [email, setEmail] = useState(""); // string, disimpulkan
    
      function ubahEmail(e: ChangeEvent<HTMLInputElement>) {
        setEmail(e.target.value);
      }
    
      function kirim(e: SubmitEvent<HTMLFormElement>) {
        e.preventDefault();
        onLogin(email.trim());
      }
    
      return (
        <form onSubmit={kirim}>
          <input type="email" value={email} onChange={ubahEmail} />
          <Tombol type="submit" disabled={email === ""}>Masuk</Tombol>
        </form>
      );
    }
    
    // 4. Pemakaian dan kesalahan yang ditangkap
    export function Halaman() {
      return (
        <Kartu judul="Login">
          <FormLogin onLogin={(email) => console.log("Login:", email)} />
          <Tombol varian="hapus">Batal</Tombol> {/* โŒ error TS2322: Type '"hapus"' is not assignable to type '"bahaya" | "utama" | undefined'. */}
        </Kartu>
      );
    }
    
    export function HalamanLain() {
      return <Kartu>Isi tanpa judul</Kartu>; // โŒ error TS2741: Property 'judul' is missing in type '{ children: string; }' but required in type 'KartuProps'.
    }
    

    Hooks dengan tipe: useState, useRef, dan useReducer:

    // Keranjang.tsx
    import { useReducer, useRef, useState } from "react";
    
    type User = { id: number; nama: string };
    type Item = { id: number; nama: string; harga: number; jumlah: number };
    
    type Aksi =
      | { type: "tambah"; item: Omit<Item, "jumlah"> }
      | { type: "kurangi"; id: number }
      | { type: "kosongkan" };
    
    function reducer(state: Item[], aksi: Aksi): Item[] {
      switch (aksi.type) {
        case "tambah": {
          const ada = state.find((i) => i.id === aksi.item.id);
          if (ada) return state.map((i) => (i.id === aksi.item.id ? { ...i, jumlah: i.jumlah + 1 } : i));
          return [...state, { ...aksi.item, jumlah: 1 }];
        }
        case "kurangi":
          return state
            .map((i) => (i.id === aksi.id ? { ...i, jumlah: i.jumlah - 1 } : i))
            .filter((i) => i.jumlah > 0);
        case "kosongkan":
          return [];
      }
    }
    
    export function Keranjang() {
      const [items, dispatch] = useReducer(reducer, []);
      const [user, setUser] = useState<User | null>(null); // nilai awal null, nanti User
      const inputCatatan = useRef<HTMLInputElement>(null);  // RefObject<HTMLInputElement | null>
    
      const total = items.reduce((t, i) => t + i.harga * i.jumlah, 0);
    
      function fokusCatatan() {
        inputCatatan.current?.focus(); // ?. karena current bisa null
      }
    
      return (
        <div>
          <p>{user ? `Halo, ${user.nama}` : "Belum login"}</p>
          <button onClick={() => setUser({ id: 1, nama: "Sari" })}>Login demo</button>
          <button onClick={() => dispatch({ type: "tambah", item: { id: 7, nama: "Kopi", harga: 25000 } })}>
            Tambah kopi
          </button>
          <button onClick={() => dispatch({ type: "kosongkan" })}>Kosongkan</button>
          <button onClick={() => dispatch({ type: "hapus", id: 7 })}>Hapus</button> {/* โŒ error TS2322: Type '"hapus"' is not assignable to type '"kosongkan" | "kurangi" | "tambah"'. */}
          <input ref={inputCatatan} placeholder="Catatan" />
          <button onClick={fokusCatatan}>Tulis catatan</button>
          <p>Total: Rp{total.toLocaleString("id-ID")}</p>
        </div>
      );
    }
    

    Perhatikan reducer tidak butuh default karena semua tipe aksi ditangani dan TypeScript tahu function selalu mengembalikan Item[]. Kalau kamu menambah aksi baru tanpa menanganinya, muncul error "Function lacks ending return statement".

    Generic component dan context bertipe:

    // DaftarGenerik.tsx
    import { createContext, useContext, useState } from "react";
    import type { ReactNode } from "react";
    
    // 1. Generic component
    type DaftarProps<T> = {
      items: T[];
      ambilKey: (item: T) => string | number;
      render: (item: T) => ReactNode;
      kosong?: ReactNode;
    };
    
    export function Daftar<T>({ items, ambilKey, render, kosong = "Tidak ada data" }: DaftarProps<T>) {
      if (items.length === 0) return <p>{kosong}</p>;
      return <ul>{items.map((item) => <li key={ambilKey(item)}>{render(item)}</li>)}</ul>;
    }
    
    type Produk = { kode: string; nama: string; stok: number };
    const produk: Produk[] = [
      { kode: "P1", nama: "Pulpen", stok: 12 },
      { kode: "P2", nama: "Map", stok: 0 },
    ];
    
    export function HalamanProduk() {
      return (
        <Daftar
          items={produk}
          ambilKey={(p) => p.kode}
          render={(p) => `${p.nama} (${p.stok > 0 ? `stok ${p.stok}` : "habis"})`} // p: Produk
        />
      );
    }
    
    // 2. Context bertipe + custom hook pengaman
    type Tema = "terang" | "gelap";
    type NilaiTema = { tema: Tema; ganti: () => void };
    
    const TemaContext = createContext<NilaiTema | null>(null);
    
    export function TemaProvider({ children }: { children: ReactNode }) {
      const [tema, setTema] = useState<Tema>("terang");
      const ganti = () => setTema((t) => (t === "terang" ? "gelap" : "terang"));
      return <TemaContext value={{ tema, ganti }}>{children}</TemaContext>;
    }
    
    export function useTema(): NilaiTema {
      const nilai = useContext(TemaContext);
      if (!nilai) throw new Error("useTema harus dipakai di dalam <TemaProvider>");
      return nilai; // bertipe NilaiTema, bukan NilaiTema | null
    }
    

    Di React 19, <TemaContext value={...}> bisa dipakai langsung sebagai provider (tidak perlu .Provider). Custom hook useTema membuat semua komponen pemakai terbebas dari pemeriksaan null.

    Kesalahan umum

    1. Memakai any untuk props atau event. Kamu kehilangan autocomplete dan pemeriksaan. Tulis tipe props, dan untuk event biarkan disimpulkan dari JSX atau salin tipe dari hover editor.
    2. useState(null) tanpa tipe. Tipe state menjadi null selamanya, dan setUser(dataUser) error. Tulis useState<User | null>(null).
    3. Mengakses ref.current tanpa memeriksa null. Saat render pertama dan setelah elemen dilepas, current bernilai null. Pakai ref.current?.focus() atau periksa dulu.
    4. Menulis ulang atribut HTML secara manual. type TombolProps = { onClick: ...; disabled: ...; type: ... } melelahkan dan pasti ada yang terlewat. Pakai ComponentProps<"button">.
    5. Memakai React.FormEvent dari tutorial lama. Di @types/react 19.3 tipe ini deprecated. Untuk onSubmit pakai SubmitEvent<HTMLFormElement>.

    Latihan

    1. Mudah, Buat komponen Badge dengan props teks: string dan warna?: "hijau" | "merah" | "abu" (default "abu").
    2. Sedang, Buat komponen InputAngka yang menerima nilai: number, onUbah: (n: number) => void, dan semua props <input> lain kecuali value, onChange, dan type (pakai Omit<ComponentProps<"input">, ...>).
    3. Menantang, Buat komponen generic Pilihan<T> (dropdown) dengan props opsi: T[], nilai: T, onPilih: (v: T) => void, label: (v: T) => string, dan ambilKey: (v: T) => string.
    Lihat solusi
    import type { ChangeEvent, ComponentProps } from "react";
    
    // 1. Mudah
    type BadgeProps = { teks: string; warna?: "hijau" | "merah" | "abu" };
    
    export function Badge({ teks, warna = "abu" }: BadgeProps) {
      const kelas = { hijau: "bg-green-100 text-green-800", merah: "bg-red-100 text-red-800", abu: "bg-gray-100 text-gray-800" };
      return <span className={`rounded px-2 py-1 text-xs ${kelas[warna]}`}>{teks}</span>;
    }
    
    // 2. Sedang
    type InputAngkaProps = Omit<ComponentProps<"input">, "value" | "onChange" | "type"> & {
      nilai: number;
      onUbah: (n: number) => void;
    };
    
    export function InputAngka({ nilai, onUbah, ...sisa }: InputAngkaProps) {
      function ubah(e: ChangeEvent<HTMLInputElement>) {
        const n = e.target.valueAsNumber;
        onUbah(Number.isNaN(n) ? 0 : n);
      }
      return <input type="number" value={nilai} onChange={ubah} {...sisa} />;
    }
    
    // 3. Menantang
    type PilihanProps<T> = {
      opsi: T[];
      nilai: T;
      onPilih: (v: T) => void;
      label: (v: T) => string;
      ambilKey: (v: T) => string;
    };
    
    export function Pilihan<T>({ opsi, nilai, onPilih, label, ambilKey }: PilihanProps<T>) {
      function ubah(e: ChangeEvent<HTMLSelectElement>) {
        const dipilih = opsi.find((o) => ambilKey(o) === e.target.value);
        if (dipilih !== undefined) onPilih(dipilih);
      }
      return (
        <select value={ambilKey(nilai)} onChange={ubah}>
          {opsi.map((o) => (
            <option key={ambilKey(o)} value={ambilKey(o)}>{label(o)}</option>
          ))}
        </select>
      );
    }
    
    // Pemakaian
    type Kota = { id: string; nama: string };
    const daftarKota: Kota[] = [{ id: "bdg", nama: "Bandung" }, { id: "jkt", nama: "Jakarta" }];
    
    export function Contoh() {
      return (
        <>
          <Badge teks="Aktif" warna="hijau" />
          <InputAngka nilai={3} onUbah={(n) => console.log(n)} min={0} placeholder="Jumlah" />
          <Pilihan
            opsi={daftarKota}
            nilai={daftarKota[0]!}
            onPilih={(k) => console.log(k.nama)}
            label={(k) => k.nama}
            ambilKey={(k) => k.id}
          />
        </>
      );
    }
    

    Ringkasan

    Video pendamping

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

    TypeScript in React - COMPLETE Tutorial (Crash Course)ByteGrad ยท Bahasa InggrisBuka di YouTube

    Kuis pemahaman

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