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 generics dan class TypeScript dan event handling 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 [hitung, setHitung] = useState(0);          // number
    const [nama, setNama] = useState("");              // string
    const [aktif, setAktif] = useState(false);        // boolean

    // Untuk nilai awal null atau undefined, beri tipe eksplisit
    const [pengguna, setPengguna] = useState<Pengguna | null>(null);
    const [daftar, 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 [form, setForm] = useState({ nama: "", email: "" });

    // ChangeEvent<HTMLInputElement>: event dari <input>
    const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setForm(prev => ({ ...prev, [name]: 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>
    );
}

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