Komponen React pada dasarnya adalah fungsi yang menerima objek props, yaitu properti yang dikirim pemanggil ke komponen, lalu mengembalikan antarmuka dalam bentuk JSX, sintaks elemen antarmuka React. Karena bentuknya demikian, seluruh peralatan tipe TypeScript dapat dipakai untuk mengamankan keduanya. Topik ini membahas pengetikan props dengan interface, varian komponen dengan union literal, pengetikan event handler, perbedaan tipe kembalian ReactNode dan JSX.Element, serta pengetikan hook useState dan useReducer. Setelah membacanya, komponen React dapat ditulis dengan kontrak tipe yang diperiksa compiler sebelum program berjalan.
Mengetik props dengan interface#
Props komponen dinyatakan sebagai tipe objek, melalui interface maupun type alias, lalu destrukturisasinya, yaitu penguraian properti objek menjadi nama-nama terpisah, dilakukan langsung pada parameter fungsi komponen. Bentuk ini memberi setiap properti tipe yang jelas, sekaligus menjadikan daftar props sebagai kontrak yang diperiksa pada setiap pemanggilan. Properti yang lupa dikirim maupun yang tidak dikenal dilaporkan compiler saat kompilasi.
// tidak-dijalankan
interface PropsTugas {
judul: string;
selesai: boolean;
}
function BarisTugas({ judul, selesai }: PropsTugas) {
return <li>{selesai ? `${judul} (selesai)` : judul}</li>;
}Fungsi BarisTugas menerima satu parameter bertipe PropsTugas, dan kedua propertinya langsung diurai menjadi dua konstanta. Tanpa anotasi tambahan, judul disimpulkan bertipe string dan selesai bertipe boolean. Pola inilah yang dipakai hampir semua komponen React, termasuk komponen yang menerima fungsi sebagai salah satu propertinya untuk dipanggil pada saat tertentu. Kesalahan pada props ditolak compiler sebelum program berjalan, dan berkas berikut sengaja mengirim tipe yang salah sekaligus properti yang tidak dikenal.
// tidak-dijalankan
interface PropsTugas {
judul: string;
selesai: boolean;
}
function labelTugas({ judul, selesai }: PropsTugas): string {
return `${selesai ? "[x]" : "[ ]"} ${judul}`;
}
console.log(labelTugas({ judul: "kirim laporan", selesai: "ya" }));
console.log(labelTugas({ judul: "arsip log", selesai: false, warna: "merah" }));# memeriksa berkas dengan dua kesalahan props
tsc --noEmit --pretty false props-salah.tsprops-salah.ts(11,50): error TS2322: Type 'string' is not assignable to type 'boolean'.
props-salah.ts(12,62): error TS2353: Object literal may only specify known properties, and 'warna' does not exist in type 'PropsTugas'.Pesan pertama menunjuk nilai "ya" pada properti selesai yang wajib bertipe boolean. Pesan kedua muncul karena warna tidak ada pada PropsTugas, dan pemeriksaan properti berlebih semacam ini berlaku untuk literal objek, yaitu bentuk yang dipakai saat memanggil komponen. Kedua pesan berasal dari satu perintah tsc, sehingga kesalahan sekecil apa pun terlihat tanpa harus menjalankan program.
Varian komponen dengan union literal#
Varian tampilan seperti tombol biasanya berjumlah terbatas, sehingga union literal, yaitu gabungan beberapa nilai yang ditulis langsung, lebih tepat daripada string bebas. Tipe VarianTombol di bawah hanya menerima tiga nilai, dan peta kelasnya dinyatakan dengan Record<VarianTombol, string> agar compiler menuntut satu kelas untuk setiap varian. Varian baru yang ditambahkan pada union tanpa kelasnya langsung dilaporkan, dan sebaliknya.
Union yang sama juga menyebar ke tipe props, sehingga komponen tombol cukup menuliskan varian: VarianTombol tanpa mendeklarasikan ulang daftar nilainya, dan setiap pemanggilnya mewarisi pemeriksaan yang sama. Pendekatan ini lebih rapi daripada menyalin daftar nilai string ke beberapa tempat, karena daftar nilai yang sah terbaca langsung dari tipenya. Refactor seperti menambah varian menjadi pekerjaan yang dipandu compiler dari awal hingga akhir.
type VarianTombol = "netral" | "utama" | "bahaya";
const KELAS_VARIAN: Record<VarianTombol, string> = {
netral: "tombol tombol-netral",
utama: "tombol tombol-utama",
bahaya: "tombol tombol-bahaya",
};
function kelasTombol(varian: VarianTombol, kecil: boolean = false): string {
const dasar = KELAS_VARIAN[varian];
return kecil ? `${dasar} tombol-kecil` : dasar;
}
console.log(kelasTombol("utama"));
console.log(kelasTombol("bahaya", true));
console.log(kelasTombol("netral", true));tombol tombol-utama
tombol tombol-bahaya tombol-kecil
tombol tombol-netral tombol-kecilFungsi kelasTombol menerima varian beserta parameter boolean kecil, lalu merangkai kelas CSS-nya dari peta. Demo di atas memanggil fungsi itu dengan tiga kombinasi yang sah, dan hasilnya selalu sesuai peta. Pemanggilan dengan nilai di luar union ditolak compiler, sebagaimana dibuktikan berkas berikut.
// tidak-dijalankan
type VarianTombol = "netral" | "utama" | "bahaya";
function kelasTombol(varian: VarianTombol): string {
return `tombol tombol-${varian}`;
}
console.log(kelasTombol("utama"));
console.log(kelasTombol("utamaa"));# memeriksa pemanggilan dengan varian yang tidak terdaftar
tsc --noEmit --pretty false varian-salah.tsvarian-salah.ts(9,25): error TS2345: Argument of type '"utamaa"' is not assignable to parameter of type 'VarianTombol'.Pesan TS2345 menyebut tipe parameter secara lengkap, termasuk union VarianTombol, sehingga pembaca pesan error langsung melihat pilihan yang sah. Kesalahan ketik seperti utamaa tertangkap di titik pemanggilan, bukan saat tombol sudah tampil dengan kelas yang tidak pernah didefinisikan. Pola ini berlaku untuk varian apa pun, mulai dari varian tombol, tingkat peringatan, hingga ukuran panel.
Event handler bertipe#
Event handler diberi tipe melalui tipe generik bawaan React, misalnya React.ChangeEvent<HTMLInputElement> untuk perubahan nilai input dan React.FormEvent<HTMLFormElement> untuk pengiriman formulir. Parameter tipe itu menunjuk elemen sumber event, sehingga event.target.value disimpulkan bertipe string tanpa penegasan apa pun. Berkas formulir berikut memperlihatkan ketiga handler dalam satu komponen.
// tidak-dijalankan
import { useState, type ChangeEvent, type FormEvent } from "react";
type TugasBaru = { judul: string; prioritas: "rendah" | "tinggi" };
function FormulirTugas({ onSimpan }: { onSimpan: (tugas: TugasBaru) => void }) {
const [judul, setJudul] = useState("");
const [prioritas, setPrioritas] = useState<TugasBaru["prioritas"]>("rendah");
function ubahJudul(event: ChangeEvent<HTMLInputElement>) {
setJudul(event.target.value);
}
function pilihPrioritas(event: ChangeEvent<HTMLSelectElement>) {
const nilai = event.target.value;
setPrioritas(nilai === "tinggi" ? "tinggi" : "rendah");
}
function kirim(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
onSimpan({ judul, prioritas });
setJudul("");
}
return (
<form onSubmit={kirim}>
<input value={judul} onChange={ubahJudul} />
<select value={prioritas} onChange={pilihPrioritas}>
<option value="rendah">rendah</option>
<option value="tinggi">tinggi</option>
</select>
<button type="submit">Simpan</button>
</form>
);
}Komponen FormulirTugas menyimpan judul dan prioritas pada dua useState, lalu mengikat tiga handler. Fungsi ubahJudul menerima ChangeEvent<HTMLInputElement> dan membaca event.target.value sebagai string, sedangkan pilihPrioritas menerima ChangeEvent<HTMLSelectElement> karena sumbernya elemen select. Nilai yang dipilih pada select datang sebagai string, sehingga perbandingan dengan "tinggi" dilakukan sebelum nilai itu dipakai sebagai prioritas. Fungsi kirim menerima FormEvent<HTMLFormElement> dan memanggil preventDefault agar formulir tidak dikirim dengan perilaku bawaan browser.
Handler yang ditulis langsung di dalam JSX boleh melewatkan anotasi, karena compiler menyimpulkan tipe parameternya dari properti onChange atau onSubmit yang diharapkan React. Pemakaian inline semacam itu tetap diperiksa ketat, karena handler pada elemen yang tidak mengenal properti terkait tetap ditolak. Anotasi eksplisit baru diperlukan ketika handler didefinisikan di luar JSX, sebagaimana dilakukan berkas di atas, supaya tipe handler tetap benar meski dipindah ke tempat lain.
Tipe kembalian dan children#
ReactNode adalah union besar yang mencakup elemen JSX, string, angka, boolean, null, undefined, hingga array berisi nilai-nilai tersebut, sedangkan JSX.Element hanya elemen JSX. Karena banyak komponen mengembalikan null atau teks pada kondisi tertentu, ReactNode lebih jarang menimbulkan penolakan dari compiler. Ketika tipe kembalian tidak dianotasi, compiler menyimpulkannya dari isi komponen. JSX.Element sendiri kini merupakan alias dari React.JSX.Element pada pengetikan modern, dan nilainya adalah objek elemen yang dihasilkan compiler JSX, bukan string maupun markup mentah. Kedua nama itu sering tertukar dalam pembahasan, dan pilihan di antara keduanya bisa diringkas dengan satu pertanyaan, yaitu apakah komponen dijanjikan mengembalikan elemen saja atau nilai yang dapat dirender apa pun.
// tidak-dijalankan
import type { ReactNode } from "react";
function Judul({ teks }: { teks: string }): ReactNode {
return <h2>{teks}</h2>;
}
function SisaKuota({ total }: { total: number }): ReactNode {
return total > 0 ? `${total} kuota tersisa` : null;
}Komponen SisaKuota mengembalikan string pada satu cabang dan null pada cabang lainnya. Tipe kembalian ReactNode menerima keduanya, sedangkan JSX.Element akan menolaknya karena string dan null bukan elemen JSX. Anotasi eksplisit semacam ini juga mendokumentasikan niat komponen bagi pembaca kodenya.
Properti children diberi tipe ReactNode karena isinya memang bisa apa pun yang dapat dirender, mulai dari teks hingga kumpulan elemen. Tipe inilah yang dipakai React untuk properti bawaan yang menampung kumpulan elemen anak. Bentuk paling ringkasnya terlihat pada komponen wadah berikut.
// tidak-dijalankan
import type { ReactNode } from "react";
function Kartu({ children }: { children: ReactNode }) {
return <section className="kartu">{children}</section>;
}Pemanggil Kartu dapat mengirim teks, elemen, atau keduanya sekaligus, dan semuanya sah menurut tipenya. Kelonggaran ReactNode justru membuat wadah semacam ini dapat dipakai ulang untuk isi apa pun tanpa menambah tipe baru. Pola children seperti inilah yang membuat komponen tata letak dapat ditulis tanpa mengetahui isi yang dikirim ke dalamnya.
Mengetik useState dan useReducer#
Hook, yaitu fungsi khusus React yang menyisipkan kemampuan seperti keadaan pada komponen fungsi, umumnya tidak perlu diberi tipe secara eksplisit. Pemanggilan useState("") menghasilkan keadaan bertipe string karena tipenya disimpulkan dari nilai awal. Saat keadaan berupa union, parameter tipe generik diperlukan, dan tipe itu dapat diambil dari tipe lain melalui akses indeks seperti TugasBaru["prioritas"] pada formulir di atas, sehingga daftar nilai prioritas tetap ditulis satu kali saja.
Untuk keadaan dengan transisi yang lebih kompleks, useReducer dipasangkan dengan fungsi reducer, yaitu fungsi yang menerima keadaan dan aksi lalu mengembalikan keadaan berikutnya. Keadaan dan aksi itu dinyatakan sebagai tipe, dan dispatch hanya menerima aksi sesuai union tersebut. Karena reducer hanyalah fungsi murni, logikanya dapat dijalankan dan diperiksa tanpa React sama sekali, seperti demo berikut.
type KeadaanAntrean = {
nomorBerikutnya: number;
riwayat: number[];
};
type AksiAntrean =
| { jenis: "panggil" }
| { jenis: "aturUlang"; mulai: number };
function pengurangAntrean(keadaan: KeadaanAntrean, aksi: AksiAntrean): KeadaanAntrean {
switch (aksi.jenis) {
case "panggil":
return {
nomorBerikutnya: keadaan.nomorBerikutnya + 1,
riwayat: [...keadaan.riwayat, keadaan.nomorBerikutnya],
};
case "aturUlang":
return { nomorBerikutnya: aksi.mulai, riwayat: [] };
}
}
let keadaan: KeadaanAntrean = { nomorBerikutnya: 1, riwayat: [] };
keadaan = pengurangAntrean(keadaan, { jenis: "panggil" });
keadaan = pengurangAntrean(keadaan, { jenis: "panggil" });
keadaan = pengurangAntrean(keadaan, { jenis: "panggil" });
console.log(keadaan);
keadaan = pengurangAntrean(keadaan, { jenis: "aturUlang", mulai: 1 });
console.log(keadaan);{ nomorBerikutnya: 4, riwayat: [ 1, 2, 3 ] }
{ nomorBerikutnya: 1, riwayat: [] }Pemanggilan pengurangAntrean menggantikan dispatch di dalam demo, dan hasilnya dicetak setelah tiga aksi panggil diikuti satu aturUlang. Pada komponen nyata, hook dipasang dengan useReducer(pengurangAntrean, { nomorBerikutnya: 1, riwayat: [] }), dan pemanggilan dispatch({ jenis: "panggil" }) diperiksa compiler terhadap AksiAntrean. Kesalahan seperti jenis: "panggilan" atau lupa menyertakan mulai pada aturUlang ditolak di titik penulisan.
Kesalahan umum#
Kesalahan pertama adalah memberi tipe pada event dengan elemen yang tidak sesuai sumbernya. Handler bertipe ChangeEvent<HTMLTextAreaElement> yang diikat ke input ditolak compiler saat disambungkan, dan tipe yang salah membuat event.target.value menunjuk bentuk yang tidak dijanjikan oleh elemen sebenarnya. Padankan parameter tipe event dengan elemen yang benar-benar memicunya. Kesalahan semacam ini biasanya muncul ketika handler disalin dari komponen lain tanpa membaca elemen sumbernya.
Kesalahan kedua adalah membuat keadaan dengan tipe yang terlalu sempit. Pemanggilan useState(null) menyimpulkan keadaan bertipe null saja, sehingga setter-nya menolak nilai lain. Keadaan yang memang boleh kosong ditulis dengan union, misalnya useState<string | null>(null).
Kesalahan ketiga adalah memakai JSX.Element sebagai tipe kembalian untuk komponen yang kadang mengembalikan null atau teks. Cabang tersebut ditolak compiler padahal perilakunya sah bagi React, sehingga ReactNode lebih aman sebagai anotasi. Bila masih ragu, biarkan compiler menyimpulkan tipe kembalian, karena inferensinya mengikuti isi komponen secara otomatis. Kesalahan keempat adalah mengganti tipe dengan any agar peringatan berhenti, karena pilihan itu mematikan pemeriksaan pada titik itu dan menyembunyikan kesalahan dari seluruh pemanggil komponen.
Ringkasan#
- Props komponen dinyatakan lewat
interfaceatau type alias, dan destrukturisasi langsung pada parameter membuat tiap properti bertipe jelas. - Union literal membatasi varian komponen pada nilai terdaftar, dan
Recordmemastikan setiap varian memiliki pemetaannya. - Event handler memakai tipe generik React seperti
ChangeEvent<HTMLInputElement>danFormEvent<HTMLFormElement>, dengan parameter tipe menunjuk elemen sumbernya. - ReactNode mencakup JSX, teks, dan null, sehingga lebih luwes daripada JSX.Element untuk tipe kembalian maupun
children. useReducerbekerja dengan fungsi reducer murni bertipe penuh, sehingga logikanya dapat diuji tanpa React.
Referensi#
- Learn TypeScript — React — Panduan resmi pengetikan props, event handler, dan hook pada komponen React.
- useState — React — Referensi hook keadaan beserta perilaku inferensi tipenya.
- Generics — The TypeScript Handbook
— Dasar parameter tipe yang dipakai
useState<T>dan tipe event generik React.