TypeScript と DOM の操作
ブラウザ開発において、DOMの操作は最も一般的なシナリオです。TypeScriptには包括的な組み込みのDOM型宣言が用意されており、要素の選択、イベント処理、フォーム操作において型安全性を提供します。
1. DOMの型システム
(1) HTMLElement の継承階層
TEXT
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) 一般的なDOMのタイプ
| タイプ | 対応する要素 | 固有の特性 |
|---|---|---|
HTMLElement |
すべてのHTML要素 | className、style、innerHTML |
HTMLInputElement |
<input> |
値、型、チェック、プレースホルダー |
HTMLButtonElement |
<button> |
無効、タイプ |
HTMLSelectElement |
<select> |
value, selectedIndex, options |
HTMLTextAreaElement |
<textarea> |
値、行数、列数 |
HTMLCanvasElement |
<canvas> |
getContext(), width, height |
HTMLFormElement |
<form> |
elements, submit(), reset() |
HTMLImageElement |
<img> |
src、alt、width、height |
(3) DOM クエリの種類
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 クエリと型の絞り込み
(1) ヌルチェック
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) 特定の要素タイプの範囲を絞り込む
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 のジェネリック
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
}
▶ 例:型安全なフォームデータの収集
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}`);
}
3. イベントの種類
(1) 一般的なイベントの種類
| イベントの種類 | トリガーとなるシナリオ | 具体的な属性 |
|---|---|---|
MouseEvent |
マウスのクリック/移動 | clientX, clientY, button |
KeyboardEvent |
キーボード入力 | key, code, ctrlKey, shiftKey |
InputEvent |
入力の変更 | data, inputType |
FocusEvent |
フォーカス変更 | relatedTarget |
SubmitEvent |
フォーム送信 | 送信者 |
ChangeEvent |
値の変更 | ターゲット |
ClipboardEvent |
クリップボード | clipboardData |
DragEvent |
ドラッグ | データ転送 |
WheelEvent |
ローラー | deltaY, deltaX |
TouchEvent |
タッチ | キー |
(2) イベントリスナーの種類
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) イベント目標の種類を絞り込む
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. フォーム操作における型安全性
(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; // 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. カスタムイベントとデータの受け渡し
(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) カスタムイベントの監視
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) 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 が指す要素)です。イベントのバブリングの過程で、この2つは異なる場合があります。一般的には、event.currentTarget を使用する方が安全です。これは、イベントリスナーをアタッチした要素だからです。Q DOM操作はパフォーマンスに大きな影響を与えますか?
A 型注釈はパフォーマンスにまったく影響を与えません(コンパイル時に消去されるためです)。DOM操作自体のパフォーマンスはコードのロジックに依存します。DOMの読み取りや書き込みが頻繁に行われると、リフローやリペイントが発生する可能性があります。TypeScriptの型システムは、より安全なDOMコードを書くのに役立ちますが、DOM操作の実行時のパフォーマンスには影響しません。
Q ブラウザ以外の環境でもDOM型は存在しますか?
A
lib には "DOM" が含まれている必要があります。Node.js にはデフォルトで DOM タイプは含まれていません。tsconfig ファイルの設定で "ES2020" のみが指定され、"DOM" が指定されていない場合、document や window などのグローバル変数を使用するとエラーが発生します。Node.js のバックエンドプロジェクトでは DOM タイプは必要ありませんが、フロントエンドプロジェクトでは必ず含める必要があります。📖 まとめ
- DOMの型階層:
HTMLElementが基底クラスであり、各要素にはそれぞれ固有のサブクラス(HTMLInputElementなど)があります。 - DOM クエリの結果は
| nullとして返されます。これを使用する前に null かどうかを確認する必要があります。特定の型を取得するには、汎用型のquerySelectorを使用してください。 - イベントの種類はイベント名から自動的に判別されます。つまり、
clickはMouseEventであり、keydownはKeyboardEventです。 - 要素固有のプロパティに安全にアクセスするには、event.target に
instanceofを付加する必要があります CustomEvent汎用型はカスタムデータを渡すことができ、Documentインターフェースを拡張して型推論を強化することができます。- フォームを扱う際、
instanceofを使用して異なる要素タイプを区別し、それぞれのプロパティに安全にアクセスする
📝 練習問題
- 基本問題(難易度 ⭐):TypeScript を使用して、
input要素とbutton要素を選択し、ボタンにクリックイベントリスナーを追加して、ボタンがクリックされたときにinputの値が出力されるようにしてください。 - 上級問題(難易度 ⭐⭐):フォーム要素を順に処理し、
requiredの入力フィールドが空かどうかを確認し、検証結果を返す関数validateForm(form: HTMLFormElement): { valid: boolean; errors: Record<string, string> }を作成してください。 - 課題(難易度:⭐⭐⭐):型安全なイベントバス—
EventBus.on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void)、EventBus.emit<K>(event: K, data: EventMap[K])—を実装してください。「user:login」(データ型User)と「cart:update」(データ型Cart)を含むEventMapを定義し、型安全性を検証してください。



