React dengan TypeScript berarti menulis komponen di file .tsx di mana props, state, ref, dan event handler punya tipe, sehingga compiler menangkap prop yang hilang atau event handler yang salah sebelum kodenya berjalan. Tipe-tipe itu hilang saat build: yang berjalan di browser adalah JavaScript yang sama dengan yang akan kamu tulis tanpa tipe. Editor live di halaman ini menjalankan JavaScript itu, dan versi bertipenya ada di samping masing-masing dalam blok statis.
Berikut Badge yang sama dengan tipe. count bersifat opsional (?), jadi badge kedua kembali ke nilai default 0, dan children menerima elemen maupun string biasa.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
Membuat proyek TypeScript
Vite punya template React plus TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
Kamu mendapat file .tsx, tsconfig.json dengan strict menyala, dan definisi tipe React (@types/react, @types/react-dom) yang sudah terinstall. Vite membuang tipe saat menyajikan dan melakukan build, tetapi tidak memeriksanya; script build milik template menjalankan tsc lebih dulu, dan editormu memeriksa selagi kamu mengetik. Framework seperti Next.js menyiapkan TypeScript dengan cara yang sama saat kamu membuat proyek dengan CLI-nya.
Memberi tipe pada props
Deskripsikan props sebagai tipe objek lalu beri anotasi pada parameter hasil destructuring. Beberapa pola sudah mencakup sebagian besar komponen:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type atau interface. Keduanya bisa untuk props. interface bisa diperluas dengan extends dan digabung dengan mendeklarasikannya dua kali; type juga bisa mengekspresikan union, intersection, dan mapped type. Sebagian besar tim memilih salah satu dan memakainya di mana-mana.
children. Pakai React.ReactNode untuk apa pun yang ditaruh di antara tag: elemen, string, angka, array, null. Halaman props menjelaskan bagaimana children bisa sampai ke komponen.
Kenapa React.FC opsional. const Badge: React.FC<BadgeProps> = (...) => ... memberi tipe pada seluruh fungsi, bukan pada parameternya. Sejak tipe React 18, ia tidak lagi menambahkan children untukmu, tidak bisa menerima parameter tipe untuk komponen generic, dan tidak menambahkan apa pun yang tidak diberikan parameter bertipe. Kode lama memakainya secara luas; fungsi biasa dengan props bertipe adalah pilihan umum untuk kode baru.
Memberi tipe pada useState
Untuk nilai awal yang sederhana, inferensi sudah cukup: useState(0) adalah number, useState('') adalah string. Kirim argumen tipe saat state bisa menampung lebih dari yang tersirat dari nilai awalnya: nilai yang dimulai sebagai null, atau salah satu dari beberapa string tetap.
Klik "Load user", dan status berpindah dari idle ke loading ke done. State bertipenya:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
Tanpa <User | null>, useState(null) menyimpulkan tipe null, dan setUser(data) gagal dikompilasi. Union itu memaksamu menangani kasus kosong, yang persis merupakan pemeriksaan user ? ... : ... di contoh yang berjalan.
useRef untuk elemen DOM
Ref yang menunjuk ke elemen DOM menerima tipe elemen dan dimulai sebagai null, karena elemennya belum ada sampai React memasangnya setelah render pertama.
Ketik sesuatu, lalu klik "Log the value": konsol mencetak apa yang kamu ketik, dibaca langsung dari elemen DOM. Versi bertipenya:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
Pakai interface elemen yang sesuai dengan tag-nya: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. Di tipe React 19, useRef selalu menerima argumen dan mengembalikan RefObject yang current-nya bisa kamu isi, jadi bentuk yang sama berlaku untuk ref DOM maupun untuk nilai seperti id timer. Lihat useRef untuk sisi runtime-nya.
Tipe event handler
Handler inline tidak butuh anotasi: di onChange={(e) => setName(e.target.value)}, TypeScript sudah tahu e dari prop tempat handler itu dikirim. Beri anotasi saat kamu memindahkan handler ke fungsinya sendiri.
Kirim form-nya, dan konsol mencatat alamatnya; preventDefault mencegah halaman dimuat ulang. Dengan tipe:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
Sejak @types/react 19.2.10, onSubmit bertipe React.SubmitEvent, dan React.FormEvent yang lebih lama ditandai deprecated; dengan versi tipe yang lebih lama, tulis React.FormEvent<HTMLFormElement> sebagai gantinya. Parameter tipenya adalah elemen tempat handler itu dipasang. Jika kamu tidak yakin dengan tipenya, arahkan kursor ke prop onChange di editormu, atau beri tipe pada seluruh handler sekaligus: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
Memberi tipe pada context
Beri createContext tipe nilainya. Jika ada nilai default yang masuk akal, kirim nilai itu, dan setiap konsumen mendapat nilai yang tidak null:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
Saat tidak ada nilai default yang bagus (user yang sedang login, sebuah store), mulai dengan null dan bungkus useContext dengan hook yang melempar error jika sebuah komponen dipakai di luar provider. Setiap pemanggil lalu mendapat tipe yang tidak null dan pesan error yang jelas, bukan crash di bagian dalam.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> adalah sintaks provider React 19; <AuthContext.Provider value={...}> diberi tipe dengan cara yang sama.
Komponen generic
List, tabel, atau select yang berfungsi dengan tipe item apa pun adalah komponen generic: sebuah parameter tipe menghubungkan prop items dengan callback renderItem.
List yang bertipe:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript menyimpulkan T dari items, jadi pemanggil tidak pernah menulis parameter tipenya. Di file .tsx, arrow function butuh <T,>, bukan <T>, karena <T> saja terbaca sebagai tag JSX.
Memperluas props elemen bawaan
Wrapper di sekitar button atau input sebaiknya menerima setiap atribut yang diterima elemen bawaannya. React.ComponentProps<'button'> adalah kumpulan lengkapnya; tambahkan props milikmu di atasnya dan sebarkan sisanya.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type, dan aria-* semuanya sudah bertipe tanpa perlu didaftarkan. Di React 19, ref adalah prop biasa, dan ComponentProps<'button'> sudah mencakupnya, jadi <Button ref={buttonRef}> sampai ke node DOM lewat spread tanpa forwardRef. Pakai ComponentPropsWithoutRef<'button'> saat kamu ingin mengecualikan ref, dan ComponentProps<typeof UserCard> untuk membaca props dari salah satu komponenmu sendiri.
Error tipe yang umum
"'ref.current' is possibly 'null'". Elemennya belum ada selama render pertama. Pakai ref.current?.focus(), atau periksa if (ref.current) sebelum memakainya.
"Type 'string' is not assignable" pada state union. const [status, setStatus] = useState('idle') menyimpulkan string, yang lebih luas dari yang kamu inginkan, dan mengirimnya ke prop bertipe Status akan gagal. Tulis useState<Status>('idle').
Event diberi tipe elemen yang salah. React.ChangeEvent<HTMLInputElement> pada select membuat e.target bertipe salah. Parameter tipenya harus sesuai dengan elemen tempat handler itu dipasang.
Memakai any untuk membungkam error. Ini mematikan pemeriksaan untuk semua yang disentuh nilai itu. Lebih baik pakai unknown lalu persempit tipenya, atau perbaiki tipenya di sumbernya.
Pertanyaan yang sering diajukan
Bagaimana cara membuat aplikasi React dengan TypeScript?
Jalankan npm create vite@latest my-app -- --template react-ts, lalu npm install dan npm run dev. Komponen ditaruh di file .tsx, dan Vite membuang tipenya saat build; jalankan tsc (script build milik template melakukannya) untuk memeriksa tipe-tipe itu.
Apa tipe untuk children di React?
React.ReactNode. Tipe ini menerima apa pun yang bisa dirender React: elemen, string, angka, array, null, dan undefined. Pakai React.ReactElement hanya saat kamu butuh tepat satu elemen.
Bagaimana cara memberi tipe useState dengan null?
Kirim tipenya secara eksplisit sebagai union: useState<User | null>(null). Tanpa argumen tipe, TypeScript menyimpulkan null sebagai satu-satunya nilai yang diizinkan.
Apa tipe event onChange di React?
React.ChangeEvent<HTMLInputElement> untuk input (HTMLTextAreaElement atau HTMLSelectElement untuk elemen-elemen itu). e.target.value lalu bertipe string.
Apakah sebaiknya saya memakai React.FC?
React.FC bersifat opsional. Memberi tipe langsung pada parameter props (function Card({ title }: CardProps)) melakukan tugas yang sama, lebih mudah dibaca, dan bisa dipakai dengan generic. React.FC tidak lagi menambahkan children secara implisit, jadi manfaat tambahannya kecil.
Sebaiknya saya memakai type atau interface untuk props?
Keduanya bisa. interface bisa diperluas dan digabung; type bisa mengekspresikan union dan mapped type. Pilih satu konvensi untuk codebase-mu dan pertahankan.