Chapter 9.3☕ 17 min read

Typing DOM Elements

Use instanceof to pick the right DOM element type — like choosing the right CCTV lens at a checkpost.

01The Right CCTV Lens

When working with the DOM in TypeScript, every element you select comes with a type. But not all types are created equal. document.getElementById() returns HTMLElement | null. document.querySelector() returns Element | null. These generic types are correct — but they don't give you access to element-specific properties like .value, .src, or .disabled. To access those, you need to narrow the type to the SPECIFIC element type.

Think of the CCTV cameras at an RTA checkpost in Hyderabad. The control room has dozens of screens showing different feeds. But you don't look at just any screen to check a specific vehicle — you choose the right camera with the right LENS:

  • The wide-angle lens shows you that there ARE vehicles (this is HTMLElement — it tells you it's a DOM element, but not what kind).
  • The zoom lens focused on the license plate shows you the specific vehicle number (HTMLInputElement — gives you access to .value).
  • The traffic light camera shows you the signal status (HTMLButtonElement — gives you .disabled).

instanceof is the tool that lets you switch from the wide-angle lens to the zoom lens. Before the check, you only know it's an HTMLElement — it could be anything. After instanceof HTMLInputElement, you KNOW it's an input, and TypeScript gives you access to all input-specific properties. Without this narrowing, you're working with the generic HTMLElement type — it tells you the element exists, but not what you can do with it.

02DOM Element Types

TypeScript comes with a comprehensive set of DOM type definitions. Every HTML element has a corresponding interface that extends HTMLElement:

// Common element types:
HTMLDivElement   // <div>
HTMLInputElement // <input>
HTMLButtonElement // <button>
HTMLAnchorElement // <a>
HTMLImageElement // <img>
HTMLFormElement  // <form>
HTMLSelectElement // <select>
HTMLTextAreaElement // <textarea>
HTMLParagraphElement // <p>
HTMLSpanElement  // <span>
HTMLUListElement // <ul>
HTMLLIElement    // <li>

// And many more — literally every
// HTML element has a type!

These types live in the lib.dom.d.ts file that ships with TypeScript. You don't need to install anything extra. They are automatically available in any TypeScript project targeting a browser environment.

Let's see how different selection methods return different types:

// getElementById — specific but narrow
const el =
  document.getElementById("app");
// Type: HTMLElement | null

// querySelector — generic
const btn =
  document.querySelector(".btn");
// Type: Element | null

// querySelectorAll — NodeList
const items =
  document.querySelectorAll(".item");
// Type: NodeListOf<Element>

// Type-specific selectors don't help
// TypeScript can't parse CSS selectors:

The key difference between HTMLElement and Element: HTMLElement extends Element and adds HTML-specific properties and methods. Element is more generic — it covers both HTML and SVG elements. In most web development, you'll be working with HTMLElement subclasses, so prefer methods that return HTMLElement (like getElementById) when possible.

03Accessing Properties Safely

Once you have an element reference, you need to access its properties. This is where type narrowing becomes essential. Let's walk through the safe patterns:

Step 1: Null Check

const input =
  document.getElementById("name");
// Type: HTMLElement | null

if (!input) {
  // Handle missing element
  console.warn("Input not found");
  return;
}
// After null check: input is
// narrowed to HTMLElement

Step 2: instanceof Check for Specific Type

if (input instanceof HTMLInputElement) {
  // input is now HTMLInputElement!
  // .value, .type, .checked, .placeholder
  // are all available!
  console.log(input.value);
  input.value = "Hello";

  // TypeScript blocks this:
  // input.src — only HTMLImageElement has .src
}

Alternative: Type Assertion (use sparingly!)

// When you're SURE of the type
const input =
  document.getElementById("name") as HTMLInputElement;
// ⚠️ No null check, no instanceof!
// Use only when you have certainty
// from external knowledge

// Slightly safer with null check:
const el =
  document.getElementById("name");
const input =
  el as HTMLInputElement | null;
if (input) {
  // Still assumes it's HTMLInputElement
  input.value;
}

Working with Multiple Elements:

const buttons =
  document.querySelectorAll("button");
// Type: NodeListOf<HTMLButtonElement>
// querySelectorAll with tag name
// gives SPECIFIC type!

buttons.forEach(btn => {
  // btn is HTMLButtonElement
  btn.disabled = true;
});

// With class selector, you lose specificity:
const els =
  document.querySelectorAll(".btn");
// Type: NodeListOf<Element>
// Must narrow each one:
els.forEach(el => {
  if (el instanceof HTMLButtonElement) {
    el.disabled = true;
  }
});

The safest and most idiomatic pattern: check for null with an early return, then narrow with instanceof to access specific properties. This pattern handles both the "element doesn't exist" case and the "wrong element type" case, giving you full type safety with zero runtime risk.

04DOM Element Traps

DOM typing in TypeScript has several gotchas that can lead to runtime errors if you're not careful. Let's walk through the most common ones.

Trap 1: Abusing the Non-Null Assertion Operator (!)

// This compiles fine but crashes at runtime
// if the element doesn't exist:
document.getElementById("app")!
  .innerHTML = "Hello";

// CORRECT: Check for null first
const app =
  document.getElementById("app");
if (app) {
  app.innerHTML = "Hello";
}

Trap 2: Forgetting that querySelector Returns Element, not HTMLElement

const el =
  document.querySelector("div");
// Type: Element | null
// Element doesn't have .style or .innerHTML
// without narrowing!

// NARROW first:
if (el instanceof HTMLElement) {
  el.style.color = "red"; // ✅ Now works
}

Trap 3: Assuming querySelector with Tag Name Gives Specific Type

const btn =
  document.querySelector("button");
// Type: Element | null
// NOT HTMLButtonElement | null!

// TypeScript can't guarantee the selector
// matches at runtime, so it returns Element.

// Fix:
if (btn instanceof HTMLButtonElement) {
  btn.disabled = true;
}

Trap 4: SVG Elements Don't Extend HTMLElement

SVG elements extend SVGElement (which extends Element), not HTMLElement. Trying to use instanceof HTMLElement on SVG elements will fail:

const svg =
  document.querySelector("svg");
// Type: Element | null

if (svg instanceof SVGSVGElement) {
  // SVG-specific properties!
  svg.viewBox.baseVal;
}

Trap 5: Creating Elements with createElement

const div =
  document.createElement("div");
// Type: HTMLDivElement ✅

// But unknown tag names:
const custom =
  document.createElement("my-component");
// Type: HTMLElement (generic)
05DOM Typing Cheatsheet

Here's your complete cheatsheet for typing DOM elements in TypeScript. Pin this to your mental board!

Selection Methods and Their Return Types:

getElementById(id)       // HTMLElement | null
querySelector(sel)       // Element | null
querySelectorAll(sel)    // NodeListOf<Element>
getElementsByClassName() // HTMLCollection
createElement(tag)       // Specific HTMLElement

Safe Access Pattern:

const el = document.getElementById("input");
if (!el) return;  // null check
if (el instanceof HTMLInputElement) {
  el.value; // ✅ Safe!
}

Common Element Types:

HTMLDivElement, HTMLInputElement,
HTMLButtonElement, HTMLAnchorElement,
HTMLImageElement, HTMLFormElement,
HTMLSelectElement, HTMLTextAreaElement,
HTMLParagraphElement, HTMLSpanElement,
SVGSVGElement  // for SVG elements

Key Rules:

  • Always null-check DOM elements — getElementById and querySelector can return null
  • Use instanceof to narrow to specific element types for property access
  • querySelector returns Element | null, not HTMLElement | null
  • SVG elements extend SVGElement, not HTMLElement — use SVGSVGElement etc.
  • Prefer getElementById or tag-specific selectors when possible
  • Avoid the non-null assertion (!) — it bypasses safety

The Golden Rule: "DOM element typing is like choosing the right CCTV lens at an RTA checkpost. HTMLElement is the wide-angle lens — it shows you something is there. instanceof HTMLInputElement is the zoom lens — it reveals the details. Use the right lens for the right job, bhai!"

Key Takeaways

  • getElementById returns HTMLElement | null — always check for null first
  • querySelector returns Element | null — narrower than HTMLElement
  • Use instanceof to narrow to specific element types like HTMLInputElement
  • Each HTML element has a corresponding TypeScript interface in lib.dom.d.ts
  • SVG elements extend SVGElement, not HTMLElement — use instanceof SVGSVGElement
  • Avoid the non-null assertion (!) — it bypasses TypeScript safety checks
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase