2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

⚡ フロントエンド基礎講座【第三回】TypeScriptでインタラクションを作ろう!DOM操作・イベント処理・型の活用

2
Posted at

1. 👋 はじめに

前回はCSSの基礎とTailwind・shadcn/ui・Bootstrapなどのフレームワークを学びました。

今回は TypeScriptを使ってWebページに動きをつける 方法を解説します!

「静的なページ」と「動くページ」の違いは何?

静的なページ:HTMLとCSSだけ → 読むだけ
動くページ:TypeScript(JavaScript)を加える → クリックに反応・データを取得・表示を切り替える

この記事を読めば:

  • ✅ DOMとは何かがわかる
  • ✅ TypeScriptでHTML要素を操作できる
  • ✅ クリック・入力などのイベント処理ができる
  • ✅ 型を使った安全なDOM操作がわかる

2. 🌳 DOMとは?

一言で言うと

DOM(Document Object Model)とは、
「HTMLをJavaScript・TypeScriptから操作できる形に変換したもの」です。

ブラウザはHTMLを読み込むと、内部でツリー構造(木構造) に変換します。
このツリーをDOMと呼び、TypeScriptから自由に操作できます。

【HTMLファイル】                【DOMツリー】

<html>                         document
  <body>                        └── html
    <h1>タイトル</h1>                 └── body
    <p>本文</p>                           ├── h1("タイトル")
    <ul>                                  ├── p("本文")
      <li>項目1</li>                      └── ul
      <li>項目2</li>                          ├── li("項目1")
    </ul>                                     └── li("項目2")
  </body>
</html>

TypeScriptからDOMを操作する = 木のノードを追加・変更・削除すること
表示を変えたり・要素を追加したり・スタイルを変更したりできます。


3. 🔍 DOM要素を取得する

まずHTMLとの対応関係を理解しよう

以下のHTMLを例に、各取得方法の違いを見ていきます。

<!-- index.html -->
<h1 id="title">ToDoリスト</h1>

<input class="search-input" type="text" placeholder="検索..." />

<button class="submit-btn">送信①</button>
<button class="submit-btn">送信②</button>

<ul id="todo-list">
  <li class="todo-item">買い物をする</li>
  <li class="todo-item">運動する</li>
  <li class="todo-item">本を読む</li>
</ul>

要素の取得方法

// ① IDで取得(1つだけ)
// → id="title" の要素を取得
const title = document.getElementById("title");
// 結果:<h1 id="title">ToDoリスト</h1> の1つ

// ② CSSセレクターで取得(最初の1つ)
// → class="submit-btn" の中で「最初の1つだけ」取得
const button = document.querySelector<HTMLButtonElement>(".submit-btn");
// 結果:<button class="submit-btn">送信①</button> のみ
//       「送信②」は無視される!

// ③ CSSセレクターで取得(すべて)
// → li タグに一致する「すべての要素」を取得
const items = document.querySelectorAll<HTMLLIElement>("li");
// 結果:[買い物をする, 運動する, 本を読む] の3つ全部
//       NodeList(配列に似たもの)として返ってくる

// ④ クラス名で取得(すべて)
const cards = document.getElementsByClassName("todo-item");
// 結果:class="todo-item" の要素すべて(3つ)

返ってくる型と使い方の違い

メソッド 返り値 取得数 主な用途
getElementById Element | null 1つ IDで特定の要素を取得
querySelector Element | null 最初の1つ セレクターで1つ取得
querySelectorAll NodeList 全部 セレクターで複数取得
// querySelectorAll は forEach でループして使う
const items = document.querySelectorAll<HTMLLIElement>("li");

items.forEach((item) => {
  item.style.color = "red"; // 全liを赤にする
});

console.log(items.length); // → 3

TypeScriptでの型指定が重要!

// ❌ 型指定なし:element は Element | null 型になる
const input = document.querySelector(".search-input");
input.value; // ❌ エラー!Element型にはvalueプロパティがない

// ✅ ジェネリクスで型を指定する
const input = document.querySelector<HTMLInputElement>(".search-input");
input?.value; // ✅ HTMLInputElement型なのでvalueにアクセスできる

// ✅ 型アサーションを使う方法
const input = document.querySelector(".search-input") as HTMLInputElement;
input.value; // ✅ OK

nullチェックを忘れずに

// getElementById は Element | null を返す
const title = document.getElementById("title");

// ❌ nullチェックなしでアクセスするとエラーになりうる
title.textContent = "新しいタイトル"; // title が null の場合クラッシュ!

// ✅ オプショナルチェーンでnullを安全に扱う
title?.textContent = "新しいタイトル"; // null なら何もしない

// ✅ if文でnullチェック
if (title) {
  title.textContent = "新しいタイトル";
}

// ✅ 型ガードで確実に処理する(最も安全)
const title = document.getElementById("title");
if (!(title instanceof HTMLElement)) {
  throw new Error("title要素が見つかりません");
}
title.textContent = "新しいタイトル"; // ここでは確実にHTMLElement

4. ✏️ DOM要素を操作する

テキスト・HTMLを変更する

const element = document.getElementById("message") as HTMLElement;

// テキストを変更(HTMLタグはそのまま文字として表示)
element.textContent = "こんにちは!";

// HTMLを変更(タグが解釈される)
element.innerHTML = "<strong>こんにちは!</strong>";

// ⚠️ innerHTML にユーザーの入力値を直接入れるのは危険!
// XSS(クロスサイトスクリプティング)攻撃のリスクがある
// → ユーザー入力には必ず textContent を使う

スタイルを変更する

const box = document.getElementById("box") as HTMLElement;

// インラインスタイルを変更
box.style.backgroundColor = "red";
box.style.fontSize = "24px";
box.style.display = "none";   // 非表示にする
box.style.display = "block";  // 表示する

クラスを追加・削除する(推奨)

const card = document.getElementById("card") as HTMLElement;

// クラスを追加
card.classList.add("active");
card.classList.add("highlight", "selected"); // 複数まとめて追加

// クラスを削除
card.classList.remove("active");

// クラスを切り替え(スイッチのON/OFFのように・クリックのたびに追加と削除を交互に繰り返す)
card.classList.toggle("open");
// → "open" がない状態でtoggle → 追加される(ON)
// → "open" がある状態でtoggle → 削除される(OFF)
// → もう一度toggle → また追加される(ON)…を繰り返す

// クラスが含まれているか確認
if (card.classList.contains("active")) {
  console.log("アクティブ状態です");
}

💡 インラインスタイルよりクラスの操作が推奨される理由

  • スタイルはCSSファイルで一元管理できる
  • Tailwindとの相性も良い
  • コードが読みやすくなる

要素を追加・削除する

// 新しい要素を作成
const newItem = document.createElement("li");
newItem.textContent = "新しい項目";
newItem.classList.add("list-item");

// 親要素に追加
const list = document.getElementById("list") as HTMLUListElement;
list.appendChild(newItem);      // 末尾に追加
list.prepend(newItem);          // 先頭に追加

// 要素を削除
const oldItem = document.getElementById("old-item") as HTMLElement;
oldItem.remove();

5. 🖱️ イベント処理

イベントとは?

イベントとは「ユーザーの操作や状況の変化」のことです。
クリック・キー入力・マウス移動・ページ読み込みなど様々なイベントがあります。

addEventListener の基本

const button = document.getElementById("btn") as HTMLButtonElement;

// クリックイベントを登録
button.addEventListener("click", (event: MouseEvent) => {
  console.log("ボタンがクリックされました!");
  console.log(event.target); // クリックされた要素
});

よく使うイベント一覧

イベント名 発生タイミング 使用例
click クリックしたとき ボタンの処理
input 入力値が変わるたびに リアルタイム検索
change 入力が確定したとき セレクトボックス
submit フォームが送信されたとき フォームのバリデーション
keydown キーを押したとき ショートカットキー
mouseover マウスが乗ったとき ホバーエフェクト
load ページ読み込み完了時 初期化処理
DOMContentLoaded DOM構築完了時 DOM操作の開始

フォームのイベント処理

const form = document.getElementById("contact-form") as HTMLFormElement;
const nameInput = document.getElementById("name") as HTMLInputElement;
const emailInput = document.getElementById("email") as HTMLInputElement;
const errorMessage = document.getElementById("error") as HTMLElement;

// フォーム送信イベント
form.addEventListener("submit", (event: SubmitEvent) => {
  // デフォルトの送信動作をキャンセル
  event.preventDefault();

  const name = nameInput.value.trim();
  const email = emailInput.value.trim();

  // バリデーション
  if (!name) {
    errorMessage.textContent = "名前を入力してください";
    errorMessage.style.display = "block";
    return;
  }

  if (!email.includes("@")) {
    errorMessage.textContent = "正しいメールアドレスを入力してください";
    errorMessage.style.display = "block";
    return;
  }

  // バリデーション通過
  errorMessage.style.display = "none";
  console.log(`送信:${name} / ${email}`);
});

// リアルタイムバリデーション
nameInput.addEventListener("input", () => {
  if (nameInput.value.trim()) {
    nameInput.classList.remove("error");
    nameInput.classList.add("valid");
  }
});

6. 🏗️ 実践:ToDoリストを作る

TypeScriptのDOM操作を総合的に練習できる定番の例です。

<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>ToDoリスト</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div id="app">
      <h1>📝 ToDoリスト</h1>
      <div class="input-area">
        <input type="text" id="todo-input" placeholder="やることを入力..." />
        <button id="add-btn">追加</button>
      </div>
      <ul id="todo-list"></ul>
    </div>
    <script src="main.js"></script>
  </body>
</html>
// main.ts

// 型定義
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

// 状態管理
let todos: Todo[] = [];
let nextId = 1;

// DOM要素の取得
const todoInput = document.getElementById("todo-input") as HTMLInputElement;
const addBtn = document.getElementById("add-btn") as HTMLButtonElement;
const todoList = document.getElementById("todo-list") as HTMLUListElement;

// ToDoを追加する関数
function addTodo(text: string): void {
  if (!text.trim()) return;

  const newTodo: Todo = {
    id: nextId++,
    text: text.trim(),
    completed: false,
  };

  todos.push(newTodo);
  renderTodos();
  todoInput.value = ""; // 入力欄をクリア
}

// ToDoを完了/未完了に切り替える関数
function toggleTodo(id: number): void {
  todos = todos.map((todo) =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo
  );
  renderTodos();
}

// ToDoを削除する関数
function deleteTodo(id: number): void {
  todos = todos.filter((todo) => todo.id !== id);
  renderTodos();
}

// リストを再描画する関数
function renderTodos(): void {
  todoList.innerHTML = "";

  todos.forEach((todo) => {
    const li = document.createElement("li");
    li.className = todo.completed ? "todo-item completed" : "todo-item";

    // ① 構造部分はinnerHTMLで作る(固定文字列のみ・ユーザー入力は含めない)
    li.innerHTML = `
      <span class="todo-text"></span>
      <div class="todo-actions">
        <button class="toggle-btn">${todo.completed ? "戻す" : "完了"}</button>
        <button class="delete-btn">削除</button>
      </div>
    `;

    // ② ユーザーが入力したテキストは必ず textContent で代入する(XSS対策!)
    const todoText = li.querySelector(".todo-text") as HTMLSpanElement;
    todoText.textContent = todo.text;

    // イベントを登録
    const toggleBtn = li.querySelector(".toggle-btn") as HTMLButtonElement;
    const deleteBtn = li.querySelector(".delete-btn") as HTMLButtonElement;

    toggleBtn.addEventListener("click", () => toggleTodo(todo.id));
    deleteBtn.addEventListener("click", () => deleteTodo(todo.id));

    todoList.appendChild(li);
  });
}

// ボタンクリックで追加
addBtn.addEventListener("click", () => {
  addTodo(todoInput.value);
});

// Enterキーでも追加できるように
todoInput.addEventListener("keydown", (event: KeyboardEvent) => {
  if (event.key === "Enter") {
    addTodo(todoInput.value);
  }
});

7. 🔄 非同期処理とDOM:データを取得して表示する

// 型定義
interface User {
  id: number;
  name: string;
  email: string;
}

// APIからデータを取得して表示する
async function fetchAndDisplayUsers(): Promise<void> {
  const container = document.getElementById("users") as HTMLElement;

  // ローディング表示
  container.textContent = "読み込み中...";

  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }

    const users: User[] = await response.json();

    // データを表示
    container.innerHTML = "";
    users.forEach((user) => {
      const div = document.createElement("div");
      div.className = "user-card";
      div.innerHTML = `
        <h3>${user.name}</h3>
        <p>${user.email}</p>
      `;
      container.appendChild(div);
    });

  } catch (error) {
    container.textContent = "データの読み込みに失敗しました";
    console.error(error);
  }
}

// ページ読み込み完了後に実行
document.addEventListener("DOMContentLoaded", () => {
  fetchAndDisplayUsers();
});

8. 💡 生TypeScriptでのDOM操作、ちょっと大変じゃありませんか?

今回作ったToDoリスト、動いたときは感動したと思います!
でもコードをよく見てみてください。

データを1つ追加・削除するたびに…

① todoList.innerHTML = ""  ← 画面を一度空っぽにする
② todos.forEach(...)       ← 全データを1からループで作り直す
③ todoList.appendChild()   ← 1つずつ追加していく

→ 1件変更するだけで「全件作り直し」という力技!

⚠️ これが大規模になると…

  • データが数千件 → 画面がカクつく
  • 「変更された要素だけ書き換えたい」 → コードが爆発的に複雑になる
  • 状態(todos配列)とUI(HTML)を手動で同期し続ける → バグの温床
😩 生DOM操作の辛さ:
  「データが変わったら、自分でHTML要素を探して、自分で更新して、
   自分で整合性を保って…」を延々と繰り返す

🤩 理想:
  「データが変わったら、自動でいい感じに画面を更新してほしい!」

このわがままな願いを圧倒的に簡単に叶えてくれるのが、次回から学ぶ React です。

今回の「生DOM操作のちょっと泥臭い大変さ」を体感したあなたなら、
Reactの便利さが 200% 理解できるはずです!🚀


8. 🎯 まとめ

概念 一言で言うと
🌳 DOM HTMLをTypeScriptから操作できるツリー構造
🔍 要素の取得 getElementByIdquerySelector で要素を取得
🏷️ 型指定 querySelector<HTMLInputElement> でアクセス可能なプロパティが明確になる
✅ nullチェック 取得した要素は必ずnullチェックが必要
✏️ 要素の操作 textContentclassListstyle で見た目を変える
🖱️ イベント addEventListener でユーザー操作に反応する
🔄 非同期 async/await でAPIからデータを取得して表示

TypeScriptでDOM操作をするポイント:
① 要素を取得するとき必ず型を指定する
② nullチェックを忘れない
③ ユーザー入力には textContent を使う(XSS対策)

次回は React × TypeScript を解説します!
「なぜReactが必要か」からTSXコンポーネント・Props・Stateの型定義まで、丁寧に解説します🌟

💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!
🎓 ここまで読んでくださって、本当にありがとうございました!


🔗 シリーズ記事

  • 【第一回】Webページの仕組み・HTML基礎
  • 【第二回】CSSで見た目を整える
  • 【第三回】TypeScriptでインタラクションを作る(この記事)
  • 【第四回】React × TypeScriptの基礎(近日公開)
  • 【第五回】React × TypeScriptの実践(近日公開)
  • 【第六回】まとめ・開発環境・次のステップ(近日公開)
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?