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から操作できるツリー構造 |
| 🔍 要素の取得 |
getElementById・querySelector で要素を取得 |
| 🏷️ 型指定 |
querySelector<HTMLInputElement> でアクセス可能なプロパティが明確になる |
| ✅ nullチェック | 取得した要素は必ずnullチェックが必要 |
| ✏️ 要素の操作 |
textContent・classList・style で見た目を変える |
| 🖱️ イベント |
addEventListener でユーザー操作に反応する |
| 🔄 非同期 |
async/await でAPIからデータを取得して表示 |
TypeScriptでDOM操作をするポイント:
① 要素を取得するとき必ず型を指定する
② nullチェックを忘れない
③ ユーザー入力にはtextContentを使う(XSS対策)
次回は React × TypeScript を解説します!
「なぜReactが必要か」からTSXコンポーネント・Props・Stateの型定義まで、丁寧に解説します🌟
💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!
🎓 ここまで読んでくださって、本当にありがとうございました!
🔗 シリーズ記事
- 【第一回】Webページの仕組み・HTML基礎
- 【第二回】CSSで見た目を整える
- 【第三回】TypeScriptでインタラクションを作る(この記事)
- 【第四回】React × TypeScriptの基礎(近日公開)
- 【第五回】React × TypeScriptの実践(近日公開)
- 【第六回】まとめ・開発環境・次のステップ(近日公開)