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
| Konsep | Cara TypeScript |
|---|---|
| Typing props | interface Props { nama: string; onKlik?: () => void } |
| Optional prop | prop?: string |
| useState explicit | useState<Tipe | null>(null) |
| Event handler | ChangeEvent<HTMLInputElement>, FormEvent<HTMLFormElement> |
| Children | children: ReactNode |
| Generic component | function 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.