TypeScript レッスン5
TypeScript総合演習
型安全なTODOアプリのコードを読み解き、TypeScriptの力を実感しよう
この演習の目標
これまで学んだTypeScriptの知識を総動員して、型安全なTODOアプリを 理解しましょう。まずTypeScriptの完全なコードを読み、その後JavaScriptで同じロジックを実際に動かします。
- interface でデータ構造を定義
- ジェネリクスで汎用的なストア関数を作成
- ユニオン型とリテラル型でフィルター機能を実装
- ユーティリティ型(Partial, Pick)を活用
- 型ガードで安全なデータ処理
TypeScript版: 型定義
まずはデータの型を定義します。TODOアプリに必要な型をinterfaceとtype aliasで宣言します。
// === 型定義 ===
// TODOの優先度(リテラル型ユニオン)
type Priority = "low" | "medium" | "high";
// TODOのフィルター
type FilterStatus = "all" | "active" | "completed";
// TODOアイテムの型
interface Todo {
readonly id: number;
title: string;
completed: boolean;
priority: Priority;
createdAt: Date;
tags?: string[];
}
// TODO作成時の入力型(idとcreatedAtは自動生成)
type CreateTodoInput = Omit<Todo, "id" | "createdAt" | "completed"> & {
completed?: boolean;
};
// TODO更新時の入力型(部分更新OK、idは変更不可)
type UpdateTodoInput = Partial<Omit<Todo, "id" | "createdAt">>;
// TODOリストの統計情報
interface TodoStats {
total: number;
active: number;
completed: number;
byPriority: Record<Priority, number>;
}TypeScript版: TODOストア
ジェネリクスを使った汎用的なIDジェネレータと、型安全なTODOストアを実装します。
// === ジェネリックなIDジェネレータ ===
function createIdGenerator(prefix: string = ""): () => number {
let nextId = 1;
return () => nextId++;
}
// === TODOストア ===
function createTodoStore() {
const todos: Todo[] = [];
const generateId = createIdGenerator();
// 追加: CreateTodoInput → Todo
function add(input: CreateTodoInput): Todo {
const todo: Todo = {
id: generateId(),
title: input.title,
completed: input.completed ?? false,
priority: input.priority,
createdAt: new Date(),
tags: input.tags,
};
todos.push(todo);
return todo;
}
// 更新: Partial で部分更新
function update(id: number, updates: UpdateTodoInput): Todo | undefined {
const index = todos.findIndex(t => t.id === id);
if (index === -1) return undefined;
todos[index] = { ...todos[index], ...updates };
return todos[index];
}
// 削除
function remove(id: number): boolean {
const index = todos.findIndex(t => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
return true;
}
// 完了トグル
function toggle(id: number): Todo | undefined {
const todo = todos.find(t => t.id === id);
if (!todo) return undefined;
return update(id, { completed: !todo.completed });
}
// フィルター: FilterStatus ユニオン型で安全
function getFiltered(status: FilterStatus): Todo[] {
switch (status) {
case "all": return [...todos];
case "active": return todos.filter(t => !t.completed);
case "completed": return todos.filter(t => t.completed);
}
}
// ソート: keyof Pick<Todo, "priority" | "createdAt"> で安全
function getSorted(by: "priority" | "createdAt"): Todo[] {
const priorityOrder: Record<Priority, number> = {
high: 0, medium: 1, low: 2
};
return [...todos].sort((a, b) => {
if (by === "priority") {
return priorityOrder[a.priority] - priorityOrder[b.priority];
}
return b.createdAt.getTime() - a.createdAt.getTime();
});
}
// 統計: TodoStats 型で構造が保証される
function getStats(): TodoStats {
return {
total: todos.length,
active: todos.filter(t => !t.completed).length,
completed: todos.filter(t => t.completed).length,
byPriority: {
high: todos.filter(t => t.priority === "high").length,
medium: todos.filter(t => t.priority === "medium").length,
low: todos.filter(t => t.priority === "low").length,
},
};
}
// タグで検索
function findByTag(tag: string): Todo[] {
return todos.filter(t => t.tags?.includes(tag) ?? false);
}
return { add, update, remove, toggle, getFiltered, getSorted, getStats, findByTag };
}JavaScript版: 実際に動かす
上のTypeScriptコードと同じロジックをJavaScriptで実装し、TODOアプリを動かしてみましょう。 TypeScriptの型がどのように安全性を提供しているか、コメントで確認できます。
Monaco + Shiki / Tab でインデント / Ctrl(Cmd)+Enter で再実行
index.htmllazy
20 lines0 issues
Monaco Editor を準備しています
表示領域に入った時点で Monaco と Shiki を初期化します。
preview.local
Live PreviewConsole
console.log / warn / error の出力がここに表示されます。
TypeScriptの型がもたらす安全性
上のJavaScriptコードでは、以下のようなバグが実行時まで見つかりません。 TypeScriptなら、これらはすべてコンパイル時にエラーになります。
// === TypeScriptがコンパイル時に防ぐバグ ===
// 1. 存在しない優先度を指定
store.add({ title: "テスト", priority: "urgent" });
// Error: '"urgent"' は '"low" | "medium" | "high"' に割り当てられない
// 2. 必須プロパティの欠落
store.add({ priority: "high" });
// Error: Property 'title' is missing
// 3. 存在しないフィルターを指定
store.getFiltered("done");
// Error: '"done"' は '"all" | "active" | "completed"' に割り当てられない
// 4. 間違った型の引数
store.toggle("1");
// Error: 'string' は 'number' に割り当てられない
// 5. 読み取り専用プロパティの変更
const todo = store.add({ title: "テスト", priority: "low" });
todo.id = 999;
// Error: Cannot assign to 'id' because it is a read-only property
// 6. 統計情報の安全な利用
const stats = store.getStats();
// stats.byPriority.urgent はエラー(Record<Priority, number>型で制限)
// 7. オプショナルチェーン
const firstTag = todo.tags?.[0];
// TypeScriptは tags が undefined の可能性を認識し、?.を強制TypeScriptコース全体の振り返り
レッスン1: 基本
- 型注釈と型推論
- 関数の引数・戻り値の型
- コンパイルの仕組み
- tsconfig.json
レッスン2: 基本の型
- string, number, boolean
- 配列とタプル
- enum, any, unknown, void, never
- ユニオン型と型エイリアス
レッスン3: インターフェース
- interface でオブジェクト型定義
- オプショナルとreadonly
- extends で拡張
- type vs interface
レッスン4: ジェネリクス
- 型引数で再利用可能なコード
- ジェネリック制約
- ユーティリティ型
- Partial, Pick, Omit, Record
次のステップ
TypeScriptの基礎を習得しました。さらにスキルアップするために、以下のことに挑戦しましょう。
- React + TypeScript — PropsやStateに型を付けてコンポーネントを作る
- Next.js + TypeScript — このアプリのように、TypeScript標準で開発する
- 高度な型 — Mapped Types, Template Literal Types, 型レベルプログラミング
- 実プロジェクト — 既存のJSプロジェクトにTypeScriptを段階的に導入する
- 型定義の読み方 — node_modules/@types を読んでライブラリの型を理解する