TypeScript: TypeScript and DOM Manipulation
Last updated: 2026-08-26
In browser development, DOM manipulation is the most common scenario—TypeScript includes comprehensive built-in DOM type declarations, providing type safety for element selection, event handling, and form manipulation.
1. The DOM Type System
(1) HTMLElement Inheritance Hierarchy
TEXT
📖 Display only
EventTarget
└─ Node
└─ Element
└─ HTMLElement
├─ HTMLInputElement (input Element)
├─ HTMLButtonElement (button Element)
├─ HTMLDivElement (div Element)
├─ HTMLSpanElement (span Element)
├─ HTMLAnchorElement (a Element)
├─ HTMLCanvasElement (canvas Element)
├─ HTMLSelectElement (select Element)
├─ HTMLTextAreaElement (textarea Element)
└─ ... More Subclasses
(2) Common DOM Types
| Type | Corresponding Element | Unique Properties |
|---|---|---|
HTMLElement |
All HTML elements | className, style, innerHTML |
HTMLInputElement |
<input> |
value, type, checked, placeholder |
HTMLButtonElement |
<button> |
disabled, type |
HTMLSelectElement |
<select> |
value, selectedIndex, options |
HTMLTextAreaElement |
<textarea> |
value, rows, cols |
HTMLCanvasElement |
<canvas> |
getContext(), width, height |
HTMLFormElement |
<form> |
elements, submit(), reset() |
HTMLImageElement |
<img> |
src, alt, width, height |
(3) Types of DOM Queries
TYPESCRIPT
// getElementById Back HTMLElement | null
let el = document.getElementById("app");
// querySelector Back Element | null
let first = document.querySelector(".item");
// querySelectorAll Back NodeListOf<Element>
let all = document.querySelectorAll(".item");
2. DOM Queries and Type Narrowing
(1) Null Checks
TYPESCRIPT
let el = document.getElementById("app");
// ❌ Use as is——It could be null
// el.innerHTML = "Hello";
// ✅ Method 1:if Inspection
if (el) {
el.innerHTML = "Hello";
}
// ✅ Method 2:Non-empty assertion(When an element is found)
el!.innerHTML = "Hello";
// ✅ Method 3:Return Early
if (!el) return;
el.innerHTML = "Hello"; // After that el Definitely not null
(2) Narrowing the Scope of Specific Element Types
TYPESCRIPT
let input = document.getElementById("email");
// input The type is HTMLElement | null——Cannot access value Properties
// input.value ❌
// ✅ Method 1:instanceof narrow
if (input instanceof HTMLInputElement) {
console.log(input.value); // ✅ HTMLInputElement has value
}
// ✅ Method 2:Type Assertion
let emailInput = document.getElementById("email") as HTMLInputElement;
console.log(emailInput.value); // ✅
// ✅ Method 3:Generic Queries(Top Recommendations)
let emailInput2 = document.querySelector<HTMLInputElement>("#email");
if (emailInput2) {
console.log(emailInput2.value); // ✅ Automatically inferred as HTMLInputElement
}
(3) querySelector generic
TYPESCRIPT
// Generic parameters specify the expected element type
let input = document.querySelector<HTMLInputElement>("input[type=email]");
let button = document.querySelector<HTMLButtonElement>("#submit");
let canvas = document.querySelector<HTMLCanvasElement>("canvas");
let form = document.querySelector<HTMLFormElement>("form");
if (input) {
console.log(input.value); // ✅ HTMLInputElement.value
}
if (canvas) {
let ctx = canvas.getContext("2d"); // ✅ CanvasRenderingContext2D | null
}
▶ Example: Type-Safe Form Data Collection
TYPESCRIPT
interface LoginForm {
email: string;
password: string;
remember: boolean;
}
function getFormData(formEl: HTMLFormElement): LoginForm {
let email = formEl.querySelector<HTMLInputElement>('input[name="email"]');
let password = formEl.querySelector<HTMLInputElement>('input[name="password"]');
let remember = formEl.querySelector<HTMLInputElement>('input[name="remember"]');
return {
email: email?.value ?? "",
password: password?.value ?? "",
remember: remember?.checked ?? false
};
}
// Usage
let form = document.querySelector<HTMLFormElement>("#login-form");
if (form) {
let data = getFormData(form);
console.log(`Email:${data.email}`);
console.log(`Remember Me:${data.remember}`);
}
Output:
TEXT
📖 Display only
Email:user@example.com
Remember Me:true
3. Event Types
(1) Common Event Types
| Event Type | Trigger Scenario | Specific Attributes |
|---|---|---|
MouseEvent |
Mouse click/movement | clientX, clientY, button |
KeyboardEvent |
Keyboard input | key, code, ctrlKey, shiftKey |
InputEvent |
Input Changes | data, inputType |
FocusEvent |
Focus change | relatedTarget |
SubmitEvent |
Form Submission | submitter |
ChangeEvent |
Value Change | target |
ClipboardEvent |
Clipboard | clipboardData |
DragEvent |
Drag | dataTransfer |
WheelEvent |
roller | deltaY, deltaX |
TouchEvent |
Touch | keys |
(2) Types of Event Listeners
TYPESCRIPT
// addEventListener The type is automatically inferred from the event name
const button = document.querySelector("button");
button?.addEventListener("click", (event) => {
// event Automatically inferred as MouseEvent
console.log(event.clientX, event.clientY); // ✅
});
document.addEventListener("keydown", (event) => {
// event Automatically inferred as KeyboardEvent
console.log(event.key); // ✅
console.log(event.ctrlKey); // ✅
if (event.key === "Enter" && event.ctrlKey) {
console.log("Ctrl+Enter Press");
}
});
(3) Narrowing the Types of Event Goals
TYPESCRIPT
function handleInput(event: Event) {
let target = event.target;
// target The type is EventTarget | null——Needs to be narrowed
if (target instanceof HTMLInputElement) {
console.log(target.value); // ✅ HTMLInputElement.value
console.log(target.type); // ✅ HTMLInputElement.type
}
}
// Abbreviation——Generic Event Handling
function onInput(event: InputEvent) {
let target = event.target as HTMLInputElement;
console.log(target.value); // ✅
}
4. Type Safety in Form Operations
(1) Iterating Through Form Elements
TYPESCRIPT
function collectForm(form: HTMLFormElement): Record<string, string | boolean> {
let data: Record<string, string | boolean> = {};
for (let element of Array.from(form.elements)) {
if (element instanceof HTMLInputElement) {
if (element.type === "checkbox") {
data[element.name] = element.checked;
} else {
data[element.name] = element.value;
}
} else if (element instanceof HTMLSelectElement) {
data[element.name] = element.value;
} else if (element instanceof HTMLTextAreaElement) {
data[element.name] = element.value;
}
}
return data;
}
(2) Form Validation
TYPESCRIPT
interface ValidationRule {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
message: string;
}
function validateInput(
input: HTMLInputElement,
rules: ValidationRule[]
): string | null {
let value = input.value;
for (let rule of rules) {
if (rule.required && !value.trim()) {
return rule.message;
}
if (rule.minLength && value.length < rule.minLength) {
return rule.message;
}
if (rule.maxLength && value.length > rule.maxLength) {
return rule.message;
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message;
}
}
return null; // Verification Passed
}
// Usage
let email = document.querySelector<HTMLInputElement>('input[name="email"]');
if (email) {
let error = validateInput(email, [
{ required: true, message: "The email address cannot be left blank." },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "The email address format is incorrect." }
]);
if (error) {
console.log(error);
}
}
5. Custom Events and Data Passing
(1) CustomEvent
TYPESCRIPT
// Create a Custom Event and Pass Data
interface CartItemAddedDetail {
itemId: number;
quantity: number;
price: number;
}
let event = new CustomEvent<CartItemAddedDetail>("cart:itemadded", {
detail: { itemId: 1, quantity: 2, price: 99.9 },
bubbles: true
});
document.dispatchEvent(event);
(2) Listening for Custom Events
TYPESCRIPT
document.addEventListener("cart:itemadded", (event) => {
// event Type automatically inferred as CustomEvent<CartItemAddedDetail>
console.log(event.detail.itemId); // ✅ number
console.log(event.detail.quantity); // ✅ number
console.log(event.detail.price); // ✅ number
});
(3) Extend the Document interface
If TypeScript does not automatically infer the type of a custom event, you can extend the Document interface:
TYPESCRIPT
interface CartEventMap {
"cart:itemadded": CustomEvent<CartItemAddedDetail>;
"cart:itemremoved": CustomEvent<{ itemId: number }>;
}
declare global {
interface Document {
addEventListener<K extends keyof CartEventMap>(
type: K,
listener: (this: Document, ev: CartEventMap[K]) => void
): void;
}
}
▶ Example: Keyboard Event Type Handling
TYPESCRIPT
function setupKeyboardShortcuts(): void {
document.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
console.log("Save triggered");
}
if (event.key === "Escape") {
let modal = document.querySelector<HTMLDialogElement>("dialog[open]");
modal?.close();
}
});
}
setupKeyboardShortcuts();
Output:
TEXT
📖 Display only
Save triggered
▶ Example: Type-Safe Canvas Rendering
TYPESCRIPT
function drawCircle(
canvas: HTMLCanvasElement,
x: number, y: number, radius: number, color: string
): void {
let ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
let canvas = document.querySelector<HTMLCanvasElement>("#myCanvas");
if (canvas) {
drawCircle(canvas, 100, 100, 50, "blue");
}
Output:
TEXT
📖 Display only
No runtime output — demonstrates type-safe canvas rendering
❓ FAQ
Q Which is better, the generic
querySelector or the as assertion?A The generic
querySelector is better—document.querySelector<HTMLInputElement>("#email") It specifies the type at the time of the query, and when used with a null check, it ensures safe usage. The as assertion skips the null check—document.getElementById("email") as HTMLInputElement If the element does not exist, the subsequent code will crash at runtime.Q What is the difference between
event.target and event.currentTarget?A
event.target is the element that actually triggered the event (which may be a child element), while event.currentTarget is the element to which the event listener is bound (the element referred to by this). These two may differ during event bubbling. It’s generally safer to use event.currentTarget—it’s the element to which you’ve attached the event listener.Q Do DOM operations have a significant impact on performance?
A Type annotations have zero impact on performance (they are erased at compile time). The performance of DOM operations themselves depends on the code logic—frequent DOM reads and writes can trigger reflows and repaints. TypeScript’s type system helps you write safer DOM code, but it does not affect the runtime performance of DOM operations.
Q Are there DOM types in non-browser environments?
A
lib must include "DOM". Node.js does not include DOM types by default—if the tsconfig file is configured with only "ES2020" and not "DOM", global variables such as document and window will result in errors. DOM types are not required for Node.js backend projects, but they must be included in frontend projects.📖 Summary
- DOM type hierarchy:
HTMLElementis the base class, and each element has its own specific subclass (such asHTMLInputElement, etc.) - A DOM query returns
| null—you must check for null before using it; use the genericquerySelectorto obtain a specific type - The event type is automatically inferred from the event name—
clickis aMouseEvent, andkeydownis aKeyboardEvent - event.target must be qualified with
instanceofto safely access element-specific properties - The
CustomEventgeneric type passes custom data and can extend theDocumentinterface to enhance type inference. - Use
instanceofto distinguish between different element types when working with forms, and safely access their respective properties
📝 Exercises
- Basic Problem (Difficulty ⭐): Using TypeScript, select an
inputelement and abuttonelement, then add a click event listener to the button so that when it is clicked, the value of theinputis printed. - Advanced Problem (Difficulty ⭐⭐): Write a function
validateForm(form: HTMLFormElement): { valid: boolean; errors: Record<string, string> }that iterates through form elements, checks whetherrequiredinput fields are empty, and returns the validation result. - Challenge (Difficulty: ⭐⭐⭐): Implement a type-safe event bus—
EventBus.on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void),EventBus.emit<K>(event: K, data: EventMap[K]). Define anEventMapcontaining "user:login" (data of typeUser) and "cart:update" (data of typeCart), and verify type safety.