Setelah paham dasar-dasar TypeScript, biasanya kamu langsung ingin membangun sesuatu yang nyata di browser. Begitu mulai, kamu pasti bertemu DOM: pohon elemen hidup yang menyusun sebuah halaman web. Kamu ambil sebuah tombol, baca isi input, lalu pasang pendengar untuk klik. Di JavaScript biasa, semua ini longgar dan gampang dimaafkan. Di TypeScript, tiba-tiba terasa lebih ketat, dan muncul garis merah di tempat yang menurutmu “jelas-jelas jalan”.
Gesekan itu bukan tanda TypeScript rewel. Itu cara TypeScript memberitahu kenyataan: apa yang sebenarnya browser kembalikan kepadamu. Setelah kamu mengenal beberapa tipe elemen dan satu aturan penting soal null, bekerja dengan DOM di TypeScript jadi tenang dan bisa ditebak. Artikel ini menjelaskan semuanya pelan-pelan, tanpa mengandaikan kamu sudah tahu banyak.
Mengingat lagi: apa itu DOM
Sebelum memberi tipe pada apa pun, mari pegang dulu konsep dasarnya. DOM (Document Object Model) adalah representasi HTML-mu di dalam memori browser. Setiap tag di halaman, dari <div> sampai <input>, berubah menjadi objek yang bisa kamu baca dan ubah lewat kode. Saat kamu menulis document.getElementById("email"), kamu sedang menjangkau pohon itu dan meminta satu objek tertentu.
Kalau ini masih baru buatmu, sebaiknya mampir dulu sebentar. Konsepnya kami bahas di Apa Itu DOM?, dan sisi praktis di JavaScript ada di DOM di JavaScript. TypeScript tidak menggantikan pengetahuan itu. Ia duduk di atasnya dan menambahkan jaring pengaman.
Setiap elemen punya tipe
Di DOM, tidak semua elemen itu jenis yang sama. Sebuah paragraf dan sebuah input memang berbagi sebagian perilaku, tetapi keduanya juga berbeda. Input punya properti value; paragraf tidak. TypeScript menangkap perbedaan ini lewat satu keluarga kecil tipe.
Di puncaknya ada HTMLElement. Ini tipe umum “elemen HTML apa saja”, dan ia membawa properti yang dimiliki semua elemen, seperti id, className, dan style. Di bawahnya ada tipe yang lebih spesifik, satu untuk hampir setiap jenis tag:
| Tipe | Cocok untuk HTML | Tambahan penting |
|---|---|---|
HTMLElement |
elemen apa saja | id, className, style |
HTMLInputElement |
<input> |
value, checked, placeholder |
HTMLButtonElement |
<button> |
disabled, type |
HTMLAnchorElement |
<a> |
href, target |
HTMLFormElement |
<form> |
submit(), elements |
HTMLImageElement |
<img> |
src, alt, width |
Polanya konsisten: HTML + nama tag + Element. Sebuah <select> menjadi HTMLSelectElement, sebuah <textarea> menjadi HTMLTextAreaElement, dan seterusnya. Kamu jarang perlu menghafal ini. Editor akan menyarankannya, dan namanya mengikuti aturan di atas.
Kenapa ini penting? Karena tipe yang spesifik membuka properti yang tepat. Kalau TypeScript hanya tahu kamu memegang sebuah HTMLElement, ia tidak akan mengizinkanmu membaca .value, sebab tidak setiap elemen punya itu. Untuk membaca nilai sebuah input dengan aman, TypeScript harus yakin elemennya memang benar-benar sebuah input.
Dari mana tipe ini berasal?
Kamu tidak pernah mendefinisikan HTMLInputElement sendiri. Tipe-tipe ini sudah dibawakan TypeScript melalui deskripsi bawaan tentang browser. Selama proyekmu menyasar DOM, tipe-tipe itu memang tersedia di mana saja.
Kenapa getElementById bisa memberimu null
Inilah baris yang mengejutkan hampir semua orang:
const input = document.getElementById("email");
// tipe input: HTMLElement | null
Bagian | null itulah inti ceritanya. TypeScript memberi tipe getElementById sebagai mungkin mengembalikan null, dan itu memang tepat. Saat kodemu berjalan, bisa saja tidak ada elemen dengan id tersebut. Mungkin HTML-nya berubah, mungkin ada salah ketik, mungkin skripnya jalan sebelum elemennya ada. Browser akan mengembalikan null, dan di JavaScript biasa baris berikutnya akan jatuh dengan galat klasik “cannot read properties of null”.
TypeScript menolak membiarkan itu lolos diam-diam. Kalau kamu coba langsung memakai input, ia menghentikanmu:
input.value = "halo@example.com";
// ^ Error: 'input' is possibly 'null'.
Ini bukan TypeScript yang menjengkelkan. Ini ia menangkap, sejak saat kamu menulis, sebuah bug yang seharusnya baru muncul di hadapan pengguna. Solusinya adalah menangani kemungkinan null secara jujur, lewat satu pemeriksaan sederhana:
const input = document.getElementById("email");
if (input) {
// Di dalam blok ini, TypeScript tahu input bukan null.
input.id; // aman
}
Ini disebut narrowing (penyempitan): begitu kamu memeriksa if (input), TypeScript mempersempit tipenya dari HTMLElement | null menjadi sekadar HTMLElement di dalam blok. Bahayanya tertangani, dan garis merahnya hilang.
Tapi perhatikan ada satu celah yang tersisa. Bahkan setelah pemeriksaan, input baru bertipe HTMLElement. TypeScript tetap tidak mengizinkanmu menyetel input.value, sebab elemen umum belum tentu punya value. Kita perlu memberitahunya bahwa elemen itu khususnya sebuah input. Di situlah assertion masuk.
Type assertion dengan as
Sebuah type assertion adalah caramu memberitahu TypeScript: “Aku tahu lebih banyak daripada kamu di sini. Percayalah, elemen ini sebuah HTMLInputElement.” Kamu menulisnya dengan kata kunci as:
const input = document.getElementById("email") as HTMLInputElement;
input.value = "jane.doe@example.com"; // sekarang diizinkan
Dengan assertion itu, TypeScript memperlakukan input sebagai elemen input, dan .value jadi tersedia. Ringkas dan pendek.
Tetapi ada jebakan yang harus kamu pahami, karena inilah cara orang paling sering salah memakai TypeScript. Sebuah assertion tidak memeriksa apa pun saat program berjalan. Ia tidak mengubah elemennya. Ia hanya mengubah apa yang TypeScript percayai. Kalau elemen dengan id email ternyata sebuah <div>, atau bahkan tidak ada sama sekali, kodemu tetap akan rusak saat dijalankan. Kamu cuma membungkam compiler tanpa membuat situasinya lebih aman sedikit pun.
`as` itu janji, bukan pemeriksaan
Saat kamu menulis as HTMLInputElement, kamu sedang mengambil tanggung jawab. TypeScript berhenti memperingatkanmu, tetapi ia tidak melakukan apa pun untuk membuktikan klaimmu. Kalau ternyata kamu salah, galatnya cuma berpindah dari saat menulis kode ke saat program berjalan, persis tempat yang tidak kamu inginkan.
Jadi kapan as sebenarnya boleh? Pakai saat kamu benar-benar tahu tipe elemennya dan TypeScript tidak bisa menyimpulkannya sendiri. Kasus yang umum dan masuk akal adalah mengambil input yang baru saja kamu tulis sendiri di HTML-mu. Tag-nya terlihat dengan mata kepalamu sendiri, jadi assertion-nya jujur.
Namun, kebiasaan yang lebih aman adalah menggabungkan pemeriksaan null dengan assertion yang lebih sempit hanya di tempat yang perlu:
const el = document.getElementById("email");
if (el instanceof HTMLInputElement) {
// Tidak perlu assertion. Ini pemeriksaan nyata saat program berjalan.
el.value = "john.doe@example.com";
}
Pemeriksaan instanceof benar-benar bertanya ke browser, saat program berjalan, “apakah ini sungguh sebuah input?” Kalau iya, TypeScript mempersempit tipenya untukmu, dan kamu tidak perlu membuat janji buta. Kalau kamu bisa memakai instanceof, utamakan itu daripada as. Pakai as hanya saat pemeriksaan nyata tidak praktis dan kamu yakin dengan markup-mu.
Catatan soal querySelector
querySelector berperilaku sedikit berbeda dan sering kali lebih enak. Ia juga bisa mengembalikan null, tetapi ia mengizinkanmu menyebut tipe yang diharapkan lewat generic:
const input = document.querySelector<HTMLInputElement>("#email");
// tipe: HTMLInputElement | null
if (input) {
input.value = "support@acy-partner.com";
}
Di sini kamu tetap menangani null, tetapi kamu terhindar dari cast as mentah. Kamu memberitahu querySelector apa yang kamu harapkan, dan kamu tetap memastikan elemennya ditemukan. Pola ini enak dibaca dan menjagamu tetap aman.
Memberi tipe pada event handler
Tempat lain TypeScript bersinar di DOM adalah event. Saat pengguna mengeklik, mengetik, atau mengirim form, browser memberikan objek event ke fungsi callback-mu. TypeScript tahu bentuk persis objek itu, dan jenis event apa yang sedang terjadi.
Mulai dari klik:
const button = document.querySelector<HTMLButtonElement>("#save");
button?.addEventListener("click", (event: MouseEvent) => {
console.log("diklik di", event.clientX, event.clientY);
});
Sebuah klik menghasilkan MouseEvent, jadi event membawa data khusus mouse seperti clientX. Perhatikan ?. setelah button: optional chaining itu berarti “panggil addEventListener hanya kalau button bukan null,” menangani kemungkinan null rapi dalam satu karakter.
Membaca apa yang diketik pengguna adalah tugas klasik, dan ada satu hal yang sudah dikenal di sini. event.target adalah elemen yang memicu event, tetapi TypeScript memberinya tipe yang luas, sebab secara teori elemen apa saja bisa jadi target. Untuk membaca .value, kamu mempersempitnya:
const field = document.querySelector<HTMLInputElement>("#search");
field?.addEventListener("input", (event: Event) => {
const target = event.target as HTMLInputElement;
console.log("pengguna mengetik:", target.value);
});
Di sini as HTMLInputElement masuk akal, karena kamu memasang pendengarnya pada sebuah input dan kamu tahu target-nya adalah input itu. Tetap saja, kalau kamu mau jalur yang lebih aman, if (event.target instanceof HTMLInputElement) sama bagusnya dan tidak butuh assertion.
Untuk form, event submit punya tipenya sendiri, dan menghentikan reload halaman bawaan biasanya jadi langkah pertama:
const form = document.querySelector<HTMLFormElement>("#signup");
form?.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
// baca field, validasi, kirim ke API-mu...
});
Biarkan inferensi bekerja
Di dalam addEventListener("click", ...), TypeScript sudah tahu event-nya adalah MouseEvent berdasarkan nama event-nya. Sering kali kamu tidak perlu menulis anotasi tipenya sama sekali. Tulis tipenya secara eksplisit hanya saat kamu mendefinisikan handler-nya terpisah dari tempat kamu memasangnya.
Menggabungkan semuanya
Berikut contoh kecil yang lengkap, yang merangkai setiap ide di atas menjadi sesuatu yang mungkin benar-benar kamu tulis: kotak pencarian yang bereaksi saat pengguna mengetik.
const form = document.querySelector<HTMLFormElement>("#search-form");
const input = document.querySelector<HTMLInputElement>("#query");
const output = document.querySelector<HTMLElement>("#result");
if (form && input && output) {
input.addEventListener("input", () => {
output.textContent = `Mencari: ${input.value}`;
});
form.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
output.textContent = `Kamu mencari: ${input.value}`;
});
}
Perhatikan apa yang membuat ini aman. Setiap elemen diambil dengan querySelector bertipe, jadi tipenya presisi. Satu pemeriksaan if (form && input && output) membereskan ketiga kemungkinan null sekaligus, dan di dalam blok itu setiap elemen sudah persis tipe yang kita butuhkan. Kita sama sekali tidak perlu menulis cast as, karena generic dan pemeriksaan null sudah menanganinya. Inilah gaya tenang dan bisa ditebak yang kamu tuju.
Rangkuman
Bekerja dengan DOM di TypeScript bermuara pada beberapa ide yang mantap. Setiap elemen punya tipe, dari HTMLElement yang umum sampai tipe spesifik seperti HTMLInputElement yang membuka properti yang tepat. Pencarian seperti getElementById bisa mengembalikan null, dan TypeScript memaksamu menanganinya, sehingga mencegah satu kelas penuh galat saat program berjalan. Type assertion dengan as adalah janji yang kamu buat ke compiler, bukan pemeriksaan nyata, jadi pakai hanya saat kamu sungguh tahu markup-nya, dan utamakan instanceof atau querySelector bertipe dengan pemeriksaan null kapan pun kamu bisa. Terakhir, event handler datang dengan tipe event yang presisi seperti MouseEvent dan SubmitEvent, dan inferensi sering kali memberinya tipe secara otomatis.
Tidak ada satu pun dari ini yang sekadar pekerjaan tambahan. Setiap aturan memetakan ke satu bug nyata yang ia hentikan sejak saat kamu menulis kode, bukan di depan pengguna. Begitu polanya terasa alami, kamu berhenti melawan garis merah dan mulai membiarkannya menuntunmu ke kode yang memang tidak gampang rusak.