404 Not Found

404 Not Found


nginx

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 汎用的な querySelectoras のアサーション、どちらが優れていますか?
A 汎用的な querySelector の方が優れています—document.querySelector<HTMLInputElement>("#email") これはクエリ実行時に型を指定し、NULLチェックと併用することで安全な使用を保証します。as アサーションは null チェックを省略します—document.getElementById("email") as HTMLInputElement 要素が存在しない場合、後続のコードは実行時にクラッシュします。
Q event.targetevent.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" が指定されていない場合、documentwindow などのグローバル変数を使用するとエラーが発生します。Node.js のバックエンドプロジェクトでは DOM タイプは必要ありませんが、フロントエンドプロジェクトでは必ず含める必要があります。

📖 まとめ

📝 練習問題

  1. 基本問題(難易度 ⭐):TypeScript を使用して、input 要素と button 要素を選択し、ボタンにクリックイベントリスナーを追加して、ボタンがクリックされたときに input の値が出力されるようにしてください。
  2. 上級問題(難易度 ⭐⭐):フォーム要素を順に処理し、requiredの入力フィールドが空かどうかを確認し、検証結果を返す関数 validateForm(form: HTMLFormElement): { valid: boolean; errors: Record<string, string> } を作成してください。
  3. 課題(難易度:⭐⭐⭐):型安全なイベントバス—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 を定義し、型安全性を検証してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%