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"documentwindow 等全局变量会报错。Node 后端项目不需要 DOM 类型,前端项目必须加上。

📖 小节

📝 作业

  1. 基础题(难度⭐):用 TypeScript 选择一个 input 元素和一个 button 元素,给 button 添加 click 事件监听器,点击时打印 input 的 value。
  2. 进阶题(难度⭐⭐):写一个函数 validateForm(form: HTMLFormElement): { valid: boolean; errors: Record<string, string> },遍历表单元素,对 required 的 input 检查是否为空,返回验证结果。
  3. 挑战题(难度⭐⭐⭐):实现类型安全的事件总线——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 类型数据),验证类型安全。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏