<CodeLearn/>
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の型がどのように安全性を提供しているか、コメントで確認できます。

index.htmllazy
20 lines0 issues

Monaco Editor を準備しています

表示領域に入った時点で Monaco と Shiki を初期化します。

preview.local
Live Preview
Console

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 を読んでライブラリの型を理解する