Langsung ke konten
KamusNgoding
Menengah React 3 menit baca

React dengan TypeScript: Komponen yang Type-Safe

#react #typescript #props #interface #useState #event-handler #reactnode #generic-component #type-safe

React dan TypeScript adalah kombinasi yang sangat populer di industri — TypeScript menangkap banyak bug sebelum runtime dan membuat refactoring lebih aman. Setelah memahami enerics dan class TypeScript](/docs/sw/typescript/generics-dan-class-typescript) dan vent handling React](/docs/sw/react/event-handling-dan-form-react), artikel ini menggabungkan keduanya untuk membangun komponen yang benar-benar type-safe.

Setup: React + TypeScript

Buat project baru dengan template TypeScript:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Perbedaan dari proyek React biasa: file menggunakan ekstensi .tsx (bukan .jsx), dan ada tsconfig.json untuk konfigurasi TypeScript.

Typing Props dengan Interface

// Tanpa TypeScript — tidak ada informasi tipe
function KartuProdukJS({ nama, harga, stok }) {
    return <div>{nama}: Rp {harga} ({stok} unit)</div>;
}
// ❌ Tidak ada autocomplete, tidak ada peringatan jika props salah

// Dengan TypeScript — props terdefinisi dengan jelas
interface PropsKartuProduk {
    nama: string;
    harga: number;
    stok: number;
    kategori?: string;       // Optional prop
    onBeli?: () => void;     // Optional callback
}

function KartuProduk({ nama, harga, stok, kategori, onBeli }: PropsKartuProduk) {
    return (
        <div style={{ border: "1px solid var(--color-border)", padding: "16px" }}>
            <h3>{nama}</h3>
            {kategori && <span style={{ color: "var(--color-text-muted)" }}>{kategori}</span>}
            <p>Rp {harga.toLocaleString("id-ID")}</p>
            <p>Stok: {stok} unit</p>
            {onBeli && (
                <button onClick={onBeli} disabled={stok === 0}>
                    {stok > 0 ? "Beli Sekarang" : "Stok Habis"}
                </button>
            )}
        </div>
    );
}

// TypeScript mencegah kesalahan penggunaan:
// <KartuProduk nama="Laptop" harga="murah" stok={3} /> // ❌ Error: harga harus number
// <KartuProduk nama="Laptop" /> // ❌ Error: harga dan stok wajib ada

// ✅ Benar:
<KartuProduk nama="Laptop ThinkPad" harga={15_000_000} stok={5} kategori="Elektronik" />

Typing useState

import { useState } from 'react';

interface Pengguna {
    id: number;
    nama: string;
    email: string;
    aktif: boolean;
}

function DaftarPengguna() {
    // TypeScript otomatis infer tipe dari nilai awal
    const itung, setHitung] = useState(0);          // number
    const ama, setNama] = useState("");              // string
    const ktif, setAktif] = useState(false);        // boolean

    // Untuk nilai awal null atau undefined, beri tipe eksplisit
    const engguna, setPengguna] = useState<Pengguna | null>(null);
    const aftar, setDaftar] = useState<Pengguna[]>([]);

    const tambahPengguna = (baru: Pengguna) => {
        setDaftar(prev => [...prev, baru]);
    };

    const pilihanPengguna = (id: number) => {
        const ditemukan = daftar.find(p => p.id === id);
        setPengguna(ditemukan ?? null); // ?? = nullish coalescing
    };

    return (
        <div>
            {pengguna && (
                <p>Dipilih: {pengguna.nama} ({pengguna.email})</p>
            )}
            <ul>
                {daftar.map(p => (
                    <li key={p.id} onClick={() => pilihanPengguna(p.id)}>
                        {p.nama} — {p.aktif ? "Aktif" : "Nonaktif"}
                    </li>
                ))}
            </ul>
        </div>
    );
}

Typing Event Handler

import { useState, ChangeEvent, FormEvent, MouseEvent } from 'react';

function FormDenganTipe() {
    const orm, setForm] = useState({ nama: "", email: "" });

    // ChangeEvent<HTMLInputElement>: event dari <input>
    const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setForm(prev => ({ ...prev, ame]: value }));
    };

    // ChangeEvent<HTMLSelectElement>: event dari <select>
    const handleSelect = (e: ChangeEvent<HTMLSelectElement>) => {
        console.log("Dipilih:", e.target.value);
    };

    // FormEvent<HTMLFormElement>: event dari <form>
    const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log("Submit:", form);
    };

    // MouseEvent<HTMLButtonElement>: event dari <button>
    const handleKlik = (e: MouseEvent<HTMLButtonElement>) => {
        console.log("Diklik pada koordinat:", e.clientX, e.clientY);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input name="nama" value={form.nama} onChange={handleInput} placeholder="Nama" />
            <input name="email" value={form.email} onChange={handleInput} placeholder="Email" />
            <select onChange={handleSelect}>
                <option value="pengguna">Pengguna</option>
                <option value="admin">Admin</option>
            </select>
            <button type="submit" onClick={handleKlik}>Kirim</button>
        </form>
    );
}

ReactNode — Typing Children

import { ReactNode } from 'react';

interface PropsKartu {
    judul: string;
    children: ReactNode;      // Bisa JSX, string, number, null, array, dll.
    aksi?: ReactNode;         // Optional footer actions
}

function Kartu({ judul, children, aksi }: PropsKartu) {
    return (
        <div style={{ border: "1px solid var(--color-border)", borderRadius: "8px" }}>
            <div style={{ padding: "16px 16px 0", borderBottom: "1px solid var(--color-border)" }}>
                <h3>{judul}</h3>
            </div>
            <div style={{ padding: "16px" }}>{children}</div>
            {aksi && (
                <div style={{ padding: "12px 16px", borderTop: "1px solid var(--color-border)" }}>
                    {aksi}
                </div>
            )}
        </div>
    );
}

// Penggunaan
function App() {
    return (
        <Kartu
            judul="Informasi Akun"
            aksi={<button>Edit Profil</button>}
        >
            <p>Nama: Ali Akbar</p>
            <p>Email: [email protected]</p>
        </Kartu>
    );
}

Komponen Reusable dengan Generics

Generics memungkinkan komponen yang bekerja dengan berbagai tipe data:

// Komponen list generic — bekerja dengan tipe data apapun
interface PropsListGeneric<T> {
    items: T[];
    renderItem: (item: T, index: number) => ReactNode;
    emptyMessage?: string;
}

function ListGeneric<T>({ items, renderItem, emptyMessage = "Tidak ada data" }: PropsListGeneric<T>) {
    if (items.length === 0) {
        return <p style={{ color: "var(--color-text-muted)" }}>{emptyMessage}</p>;
    }

    return (
        <ul style={{ listStyle: "none", padding: 0 }}>
            {items.map((item, index) => (
                <li key={index} style={{ padding: "8px 0", borderBottom: "1px solid var(--color-border)" }}>
                    {renderItem(item, index)}
                </li>
            ))}
        </ul>
    );
}

// Penggunaan dengan berbagai tipe:
interface Produk { id: number; nama: string; harga: number; }
interface Artikel { slug: string; judul: string; tanggal: string; }

function App() {
    const produk: Produk[] = [
        { id: 1, nama: "Laptop", harga: 15_000_000 },
        { id: 2, nama: "Mouse", harga: 250_000 },
    ];

    const artikel: Artikel[] = [
        { slug: "react-ts", judul: "React dengan TypeScript", tanggal: "2026-04-05" },
        { slug: "hooks-guide", judul: "Panduan Lengkap Hooks", tanggal: "2026-04-03" },
    ];

    return (
        <div>
            <h2>Produk</h2>
            <ListGeneric
                items={produk}
                renderItem={(p) => (
                    <span>{p.nama} — Rp {p.harga.toLocaleString("id-ID")}</span>
                )}
                emptyMessage="Tidak ada produk"
            />

            <h2>Artikel</h2>
            <ListGeneric
                items={artikel}
                renderItem={(a) => (
                    <a href={`/docs/${a.slug}`}>{a.judul}</a>
                )}
            />
        </div>
    );
}

Troubleshooting: Error yang Sering Muncul

Type '...' is not assignable to type 'IntrinsicAttributes & Props'

Penyebab: Props yang dikirim tidak sesuai dengan interface — typo nama prop, tipe salah, atau props yang tidak ada di interface.

Solusi:

interface KartuProps {
  judul: string;
  nilai: number;
}

// Error: 'title' tidak ada di KartuProps
<Kartu title="Test" nilai={5} />

// Gunakan nama yang sesuai interface
<Kartu judul="Test" nilai={5} />

// Jika props memang opsional, tandai dengan `?`
interface KartuProps {
  judul: string;
  nilai?: number; // opsional — boleh tidak dikirim
}

Property 'children' does not exist on type 'Props'

Penyebab: Komponen menerima children tapi interface props tidak mendefinisikannya secara eksplisit.

Solusi:

// Error: TypeScript tidak tahu komponen menerima children
interface ContainerProps {
  className: string;
}
function Container({ className, children }: ContainerProps) { ... }

// Tambahkan children ke interface
interface ContainerProps {
  className: string;
  children: React.ReactNode; // tipe paling fleksibel untuk children
}

// Alternatif: gunakan React.PropsWithChildren
type ContainerProps = React.PropsWithChildren<{ className: string }>;

Object is possibly 'null' saat menggunakan useRef

Penyebab: TypeScript tahu bahwa ref bisa null sebelum komponen mount — harus dicek sebelum diakses.

Solusi:

const inputRef = useRef<HTMLInputElement>(null);

// Error: Object is possibly 'null'
inputRef.current.focus();

// Periksa null dengan optional chaining atau if
inputRef.current?.focus();

// Atau gunakan non-null assertion (jika yakin sudah mount)
useEffect(() => {
  inputRef.current!.focus(); // ! memberitahu TS bahwa kita yakin tidak null
}, []);

Pertanyaan yang Sering Diajukan

Apa perbedaan React.FC dan function biasa untuk komponen?

React.FC<Props> (atau React.FunctionComponent) adalah tipe bawaan React untuk function component. Namun, komunitas modern cenderung tidak menggunakannya karena: (1) implisit menambahkan children ke props (sudah dihapus di React 18), (2) tidak mendukung generics dengan baik. Lebih baik tulis fungsi biasa dengan typing props eksplisit: function Komponen({ prop }: Props) {...}.

Mengapa useState<Pengguna | null>(null) dan bukan useState(null)?

Jika menulis useState(null), TypeScript menyimpulkan tipe sebagai null — dan setPengguna hanya menerima null. Dengan useState<Pengguna | null>(null), TypeScript tahu state bisa Pengguna atau null, sehingga setPengguna(dataPengguna) bisa menerima objek Pengguna yang valid. Prinsip: beri tipe eksplisit saat nilai awal tidak mencerminkan semua kemungkinan nilai.

Kapan menggunakan interface vs type untuk props?

Keduanya bisa digunakan untuk props. Konvensi umum: gunakan interface untuk object shapes (props, API response) karena bisa di-extend dan lebih jelas di error message. Gunakan type untuk union types, intersection, atau alias tipe primitif. Untuk konsistensi, pilih satu dan gunakan secara konsisten dalam satu proyek.

Bagaimana cara typing hook useReducer?

Buat type untuk state dan union type untuk action: type Action = { type: "increment" } | { type: "set"; value: number }. Kemudian useReducer<React.Reducer<State, Action>>(reducer, initialState). TypeScript akan memeriksa bahwa dispatch hanya menerima action yang valid dan reducer menangani semua tipe action.

Kesimpulan

KonsepCara TypeScript
Typing propsinterface Props { nama: string; onKlik?: () => void }
Optional propprop?: string
useState explicituseState<Tipe | null>(null)
Event handlerChangeEvent<HTMLInputElement>, FormEvent<HTMLFormElement>
Childrenchildren: ReactNode
Generic componentfunction Comp<T>({ items }: { items: T[] })

Artikel sebelumnya: Event Handling dan Form di React — interaksi pengguna dan form.

Selamat! Kamu sudah menguasai React dari dasar hingga TypeScript integration. Untuk mendalami lebih lanjut, pelajari state management global dengan Zustand atau Redux Toolkit yang keduanya memiliki dukungan TypeScript yang sangat baik.

Artikel Terkait