TypeScript: TypeScript 与 DOM 操作
最后更新:2026-08-26
浏览器开发中,DOM 操作是最常见的场景——TypeScript 内置了完整的 DOM 类型声明,让元素选择、事件处理、表单操作都有类型保护。
1. DOM 类型体系
(1) HTMLElement 继承层次
TEXT
📖 仅展示
EventTarget
└─ Node
└─ Element
└─ HTMLElement
├─ HTMLInputElement (input 元素)
├─ HTMLButtonElement (button 元素)
├─ HTMLDivElement (div 元素)
├─ HTMLSpanElement (span 元素)
├─ HTMLAnchorElement (a 元素)
├─ HTMLCanvasElement (canvas 元素)
├─ HTMLSelectElement (select 元素)
├─ HTMLTextAreaElement (textarea 元素)
└─ ... 更多子类
(2) 常用 DOM 类型
| 类型 | 对应元素 | 特有属性 |
|---|---|---|
HTMLElement |
所有 HTML 元素 | 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) DOM 查询的类型
TYPESCRIPT
// getElementById 返回 HTMLElement | null
let el = document.getElementById("app");
// querySelector 返回 Element | null
let first = document.querySelector(".item");
// querySelectorAll 返回 NodeListOf<Element>
let all = document.querySelectorAll(".item");
2. DOM 查询与类型收窄
(1) null 检查
TYPESCRIPT
let el = document.getElementById("app");
// ❌ 直接使用——可能是 null
// el.innerHTML = "Hello";
// ✅ 方式一:if 检查
if (el) {
el.innerHTML = "Hello";
}
// ✅ 方式二:非空断言(确定元素存在时)
el!.innerHTML = "Hello";
// ✅ 方式三:提前返回
if (!el) return;
el.innerHTML = "Hello"; // 此后 el 一定非 null
(2) 具体元素类型的收窄
TYPESCRIPT
let input = document.getElementById("email");
// input 类型是 HTMLElement | null——不能访问 value 属性
// input.value ❌
// ✅ 方式一:instanceof 收窄
if (input instanceof HTMLInputElement) {
console.log(input.value); // ✅ HTMLInputElement 有 value
}
// ✅ 方式二:类型断言
let emailInput = document.getElementById("email") as HTMLInputElement;
console.log(emailInput.value); // ✅
// ✅ 方式三:泛型查询(最推荐)
let emailInput2 = document.querySelector<HTMLInputElement>("#email");
if (emailInput2) {
console.log(emailInput2.value); // ✅ 自动推断为 HTMLInputElement
}
(3) querySelector 泛型
TYPESCRIPT
// 泛型参数指定期望的元素类型
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
}
▶ 示例:类型安全的表单收集
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
};
}
// 使用
let form = document.querySelector<HTMLFormElement>("#login-form");
if (form) {
let data = getFormData(form);
console.log(`邮箱:${data.email}`);
console.log(`记住我:${data.remember}`);
}
3. 事件类型
(1) 常用事件类型
| 事件类型 | 触发场景 | 特有属性 |
|---|---|---|
MouseEvent |
鼠标点击/移动 | clientX, clientY, button |
KeyboardEvent |
键盘操作 | key, code, ctrlKey, shiftKey |
InputEvent |
输入变化 | data, inputType |
FocusEvent |
焦点变化 | relatedTarget |
SubmitEvent |
表单提交 | submitter |
ChangeEvent |
值改变 | target |
ClipboardEvent |
剪贴板 | clipboardData |
DragEvent |
拖拽 | dataTransfer |
WheelEvent |
滚轮 | deltaY, deltaX |
TouchEvent |
触摸 | touches |
(2) 事件监听的类型
TYPESCRIPT
// addEventListener 的类型由事件名自动推断
const button = document.querySelector("button");
button?.addEventListener("click", (event) => {
// event 自动推断为 MouseEvent
console.log(event.clientX, event.clientY); // ✅
});
document.addEventListener("keydown", (event) => {
// event 自动推断为 KeyboardEvent
console.log(event.key); // ✅
console.log(event.ctrlKey); // ✅
if (event.key === "Enter" && event.ctrlKey) {
console.log("Ctrl+Enter 按下");
}
});
(3) 事件目标的类型收窄
TYPESCRIPT
function handleInput(event: Event) {
let target = event.target;
// target 类型是 EventTarget | null——需要收窄
if (target instanceof HTMLInputElement) {
console.log(target.value); // ✅ HTMLInputElement.value
console.log(target.type); // ✅ HTMLInputElement.type
}
}
// 简写——泛型事件处理
function onInput(event: InputEvent) {
let target = event.target as HTMLInputElement;
console.log(target.value); // ✅
}
4. 表单操作的类型安全
(1) 表单元素遍历
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) 表单验证
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; // 验证通过
}
// 使用
let email = document.querySelector<HTMLInputElement>('input[name="email"]');
if (email) {
let error = validateInput(email, [
{ required: true, message: "邮箱不能为空" },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "邮箱格式不正确" }
]);
if (error) {
console.log(error);
}
}
5. 自定义事件与数据传递
(1) CustomEvent
TYPESCRIPT
// 创建自定义事件并传递数据
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) 监听自定义事件
TYPESCRIPT
document.addEventListener("cart:itemadded", (event) => {
// event 类型自动推断为 CustomEvent<CartItemAddedDetail>
console.log(event.detail.itemId); // ✅ number
console.log(event.detail.quantity); // ✅ number
console.log(event.detail.price); // ✅ number
});
(3) 扩展 Document 接口
如果 TypeScript 没有自动推断自定义事件类型,可以扩展 Document 接口:
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;
}
}
❓ 常见问题
Q querySelector 泛型和 as 断言哪个好?
A querySelector 泛型更好——
document.querySelector<HTMLInputElement>("#email") 在查询时就指定类型,配合 null 检查就能安全使用。as 断言跳过了 null 检查——document.getElementById("email") as HTMLInputElement 如果元素不存在,后续代码会运行时崩溃。Q event.target 和 event.currentTarget 有什么区别?
A
event.target 是实际触发事件的元素(可能是子元素),event.currentTarget 是绑定事件监听器的元素(this 指向的元素)。事件冒泡时两者可能不同。通常用 event.currentTarget 更安全——它是你绑定事件的那个元素。Q DOM 操作的性能影响大吗?
A 类型注解零性能影响(编译时擦除)。DOM 操作本身的性能取决于代码逻辑——频繁的 DOM 读写会触发重排/重绘。TypeScript 的类型系统能帮你写出更安全的 DOM 代码,但不会改变 DOM 操作的运行时性能。
Q 非浏览器环境有 DOM 类型吗?
A 需要
lib 包含 "DOM"。Node.js 默认不包含 DOM 类型——如果 tsconfig 只配了 "ES2020" 而没有 "DOM",document、window 等全局变量会报错。Node 后端项目不需要 DOM 类型,前端项目必须加上。📖 小节
- DOM 类型体系:HTMLElement 是基类,各元素有具体的子类(HTMLInputElement 等)
- DOM 查询返回
| null——必须先检查 null 再使用;用泛型 querySelector 获得具体类型 - 事件类型由事件名自动推断——click 是 MouseEvent,keydown 是 KeyboardEvent
- event.target 需要 instanceof 收窄才能安全访问元素特有属性
- CustomEvent 泛型传递自定义数据,可以扩展 Document 接口增强类型推断
- 表单操作用 instanceof 区分不同元素类型,安全访问各自特有属性
📝 作业
- 基础题(难度⭐):用 TypeScript 选择一个 input 元素和一个 button 元素,给 button 添加 click 事件监听器,点击时打印 input 的 value。
- 进阶题(难度⭐⭐):写一个函数
validateForm(form: HTMLFormElement): { valid: boolean; errors: Record<string, string> },遍历表单元素,对 required 的 input 检查是否为空,返回验证结果。 - 挑战题(难度⭐⭐⭐):实现类型安全的事件总线——
EventBus.on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void),EventBus.emit<K>(event: K, data: EventMap[K])。定义一个 EventMap 包含 "user:login"(User 类型数据)和 "cart:update"(Cart 类型数据),验证类型安全。