Mert Özen Her Satırda Daha İleri
Frontend TypeScript Temel — #14

Tarayıcıda TypeScript: DOM Tipleri, querySelector ve Event'ler

Mert Özen 3 Kas 2026 7 dk 2 görüntülenme
Tarayıcıda TypeScript: DOM Tipleri, querySelector ve Event'ler

lib.dom tiplerini, querySelector'ın dönüş tiplerini ve generic kullanımını, HTMLInputElement gibi element type'larını, event tiplerini ve event.target tuzağını görüyor, tsc çıktısını bir module script ile sayfada çalıştırıyoruz.

Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.

Önce Tahmin Et

Bir sayfada bir buton ve id'si email olan bir input var. JavaScript serilerinde yüzlerce kez yazdığın iki satır:

const button = document.querySelector("button");
const email = document.querySelector("#email");

console.log(email.value);

İki soru: TypeScript button ve email için hangi type'ı çıkarır? Son satır derlenir mi? İpucu: iki değişkenin type'ı aynı değil.

lib.dom: Tarayıcının Tip Haritası

tsconfig yazısında, lib yazmadığında TypeScript'in ECMAScript tiplerinin yanına tarayıcının dom tiplerini de kattığını görmüştük. Bu tipler TypeScript paketiyle birlikte gelir ve web standartlarından üretilir: document, window, localStorage, fetch, her HTML element'i ve her event için hazır bir type vardır. Hiçbir paket kurmadan editörün document. yazdığın anda yüzlerce property ve method önerebiliyorsa, sebebi bu dosyadır.

Bu tipler bir hiyerarşi oluşturur. En üstte EventTarget var; onun altında Node, sonra Element, sonra HTMLElement, en altta da HTMLInputElement, HTMLButtonElement gibi somut element'ler. Aşağı indikçe type daha çok şey bilir: Element'te value yoktur, çünkü her element'in bir değeri olmaz; HTMLInputElement'te vardır.

Bir şeyi baştan netleştirelim: lib.dom tarayıcının API'lerini tarif eder, ama sayfandaki HTML'i bilmez. #email diye bir element'in var olduğunu ya da onun bir input olduğunu TypeScript'e söyleyen hiçbir şey yoktur. Bu yazıdaki bütün dikkatli kontrollerin sebebi bu.

querySelector Ne Döndürür?

Cevap selector'a bağlı. Selector yalnızca bir etiket adıysa TypeScript onu bir tabloda arar ve doğru element type'ını verir. Diğer bütün durumlarda elinde yalnızca genel Element kalır:

const heading = document.querySelector("h1"); // HTMLHeadingElement | null
const items = document.querySelectorAll("li"); // NodeListOf<HTMLLIElement>
const logo = document.querySelector("svg"); // SVGSVGElement | null
const app = document.getElementById("app"); // HTMLElement | null
const card = document.querySelector(".card"); // Element | null

".card" gibi bir class selector'ın hangi element'i bulacağını TypeScript bilemez; o yüzden Element der. Sonuçlarda | null görmen de tesadüf değil: querySelector hiçbir şey bulamazsa null döndürür ve strict mod bunu senden saklamaz. querySelectorAll ise her zaman bir liste döndürür, belki boş bir liste, bu yüzden onda null yok.

Açılıştaki cevap da burada: button, HTMLButtonElement | null; email ise Element | null. Son satır derlenmez ve tsc iki hata verir:

error TS18047: 'email' is possibly 'null'.
error TS2339: Property 'value' does not exist on type 'Element'.

Doğru Element Tipine Ulaşmak

Elinde Element | null var, sana HTMLInputElement lazım. Üç yol var ve aralarındaki fark önemli. İlk ikisi şöyle:

const email = document.querySelector<HTMLInputElement>("#email");
// type'ı: HTMLInputElement | null

const password = document.querySelector("#password") as HTMLInputElement;
// type'ı: HTMLInputElement

Birincisi, querySelector'ın generic kullanımı. Açılı parantezler içindeki type, fonksiyona "bana bu element type'ını döndür" der. Generic'leri Orta serinin ilk yazısında baştan anlatacağız; şimdilik bunu, dönüş tipini seçtiğin bir ayar gibi oku. null olasılığı yerinde kalır, bu iyi bir şey. İkincisi, type assertion yazısında gördüğümüz as. Bu hem type'ı değiştirir hem de null'ı sessizce siler.

İkisinin ortak sorunu, hiçbirinin çalışma zamanında bir şey kontrol etmemesi. #email aslında bir textarea ise kodun derlenir, ama yanlış varsayımla çalışır. Üçüncü yol bu açığı kapatır:

const email = document.querySelector("#email");

if (email instanceof HTMLInputElement) {
  console.log(email.value.trim());
} else {
  console.error("Missing #email input");
}

Narrowing yazısında gördüğümüz instanceof, burada iki işi birden yapar: null'ı eler ve element'in gerçekten bir input olduğunu çalışma zamanında doğrular. if bloğunun içinde email'in type'ı HTMLInputElement'tir. Benim varsayılanım bu yol; kendi yazdığım ve emin olduğum küçük sayfalarda generic kullanımı da kabul edilebilir.

HTMLInputElement ve Akrabaları

Doğru type'a ulaştığında editör sana o element'e özgü her şeyi gösterir. Input'ta value, valueAsNumber, checked, disabled; HTMLSelectElement'te value ve selectedIndex; HTMLFormElement'te elements ve reset(). Bir tanesine özellikle dikkat et:

const price = document.querySelector("#price");
const count = document.querySelector("#count");

if (price instanceof HTMLInputElement && count instanceof HTMLInputElement) {
  const wrong = price.value + count.value; // string: "10" + "2" → "102"
  const right = price.valueAsNumber + count.valueAsNumber; // number: 12
  console.log(wrong, right);
}

type="number" olan bir input'ta bile value her zaman string'dir. TypeScript bunu type'ında açıkça söyler, bu yüzden wrong'un type'ı string olur ve fark etmeden toplama yerine birleştirme yaptığını hover ile hemen görürsün. Sayı istiyorsan valueAsNumber kullan; boş bir input'ta bunun NaN döndürdüğünü de unutma.

Event Tipleri

addEventListener'a event adını string olarak verdiğinde TypeScript callback'in parametresinin type'ını bu addan çıkarır. Hiç annotation yazman gerekmez:

const button = document.querySelector("button");

button?.addEventListener("click", (event) => {
  console.log(event.clientX, event.clientY); // event: PointerEvent
});

document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    console.log("Closed"); // event: KeyboardEvent
  }
});

"keydown" için KeyboardEvent, "submit" için SubmitEvent, "input" için genel Event gelir. "click"'te ise eski kaynaklarda MouseEvent yazdığını görürsün; TypeScript 7.0.2'nin lib.dom'unda type PointerEvent'tir. PointerEvent, MouseEvent'i genişlettiği için clientX gibi bütün tanıdık property'ler yerinde.

Callback'i ayrı bir fonksiyon olarak yazdığında bu çıkarım çalışmaz, çünkü fonksiyon tanımlandığı yerde hangi event'e bağlanacağını bilmez. O zaman parametreye annotation yazarsın ve TypeScript yanlış eşleşmeyi yakalar:

function handleKey(event: KeyboardEvent) {
  if (event.key === "Escape") {
    console.log("Closed");
  }
}

document.addEventListener("keydown", handleKey);

// Hata: No overload matches this call.
document.addEventListener("click", handleKey);

Hata mesajının alt satırları sebebi de açıklar: Type 'Event' is missing the following properties from type 'KeyboardEvent': altKey, charCode, code, ctrlKey, and 17 more. Bir tıklama event'inde key property'si yoktur ve TypeScript bunu çalışmadan önce söyler.

Sık Düşülen Tuzaklar

En klasik tuzak event.target. Bir listeye tek bir listener koyup hangi li'ye tıklandığını bulmak istiyorsun:

const list = document.querySelector("ul");

list?.addEventListener("click", (event) => {
  // Hata: 'event.target' is possibly 'null'.
  // Hata: Property 'closest' does not exist on type 'EventTarget'.
  const item = event.target.closest("li");
});

event'in type'ı PointerEvent, ama event.target'ınki yalnızca EventTarget | null. Sebebi şu: event'i başlatan şey bir element olmak zorunda değil; window, document ya da bir XMLHttpRequest da event yayabilir. TypeScript genel addEventListener imzasında hedefin ne olduğunu bilemez. Çözüm yine instanceof:

const list = document.querySelector("ul");

list?.addEventListener("click", (event) => {
  if (event.target instanceof Element) {
    const item = event.target.closest("li");
    console.log(item?.textContent);
  }
});

İkinci tuzak, generic kullanımına fazla güvenmek. document.querySelector<HTMLInputElement>("button") hatasız derlenir; TypeScript yalnızca type'ın bir Element olmasını ister, selector ile tutarlı olup olmadığını kontrol etmez. Yani bu da bir assertion, sadece daha kibar yazılmış hâli.

Üçüncü tuzak, her querySelector'ın sonuna ! eklemek. Sayfanın HTML'i değiştiğinde ya da script beklediğinden erken çalıştığında, derleme zamanında sustuğun hata çalışma zamanında TypeError olarak geri döner.

tsc ile Derleyip Sayfada Çalıştırmak

Tarayıcılar TypeScript çalıştırmaz; sayfaya JavaScript vermemiz gerekiyor. Küçük bir fiyat hesaplayıcı yapalım. Klasör yapısı şöyle:

price-calculator/
  index.html
  tsconfig.json
  src/
    format.ts
    main.ts

tsconfig yazısındaki ayarların tarayıcı için uyarlanmış hâli:

{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

module ve lib'i yazmadık; varsayılan esnext modülleri ve dom tipleri tarayıcı için tam istediğimiz şey. TS 5.7 ile gelen rewriteRelativeImportExtensions, import'lardaki .ts uzantısını çıktıda .js'e çevirir; tarayıcı dist klasöründe gerçekten var olan dosyayı ister. İki kaynak dosyamız şunlar:

// src/format.ts
export function formatPrice(amount: number): string {
  return `$${amount.toFixed(2)}`;
}
// src/main.ts
import { formatPrice } from "./format.ts";

const unitPrice = 12.5;
const quantity = document.querySelector("#quantity");
const total = document.querySelector("#total");

if (quantity instanceof HTMLInputElement && total instanceof HTMLOutputElement) {
  quantity.addEventListener("input", () => {
    total.value = formatPrice(quantity.valueAsNumber * unitPrice);
  });
}

Ve sayfa:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Price Calculator</title>
    <script type="module" src="./dist/main.js"></script>
  </head>
  <body>
    <input id="quantity" type="number" value="1" min="1">
    <output id="total">$12.50</output>
  </body>
</html>

Klasörde npx tsc çalıştırdığında dist/main.js ve dist/format.js oluşur; main.js'teki import satırı artık "./format.js"'i gösterir. Script'i <head>'e koymamıza rağmen element'leri bulabiliyoruz, çünkü JavaScript serilerinde gördüğümüz gibi type="module" script'ler HTML okunup bitene kadar bekler. Yine o serilerden hatırlarsın: sayfayı file:// ile açarsan module yüklenmez, Live Server gibi bir yerel sunucu kullan. Geliştirirken npx tsc --watch çalıştırırsan her kaydettiğinde çıktı yenilenir.

Küçük Bir Deneme

★☆☆ Type'ı ne olur, tahmin et: document.querySelector("form"), document.querySelector("#signup") ve document.querySelectorAll("button"). Editörde üzerine gelerek doğrula.

★★☆ Fiyat hesaplayıcıdaki input'u boşalt; total'da ne görüyorsun? valueAsNumber'ın NaN olduğu durumda $0.00 gösterecek şekilde kodu düzelt.

★★☆ Derlenir mi: document.addEventListener("submit", (event: KeyboardEvent) => {});? Hata alırsan mesajı oku ve doğru event type'ını bul.

★★★ Bir ul içindeki her li'ye tıklandığında o element'in metnini konsola yazan kodu, tek bir listener ve event.target ile, hiç as ya da ! kullanmadan yaz.

TypeScript Temel serisi burada bitiyor. Artık tipleri okuyabiliyor, kendi type'larını yazabiliyor, derleyiciyi yapılandırabiliyor ve TypeScript'i tarayıcıda kullanabiliyorsun. Bu yazıda querySelector'ın açılı parantezli kullanımıyla generic'lerin kapısını araladık. TypeScript Orta serisinde o kapıdan gireceğiz: generic'leri baştan anlatacak, ardından class'ları, Partial ve Pick gibi utility type'ları, kendi type guard'larını yazmayı ve modülleri konuşacağız.