TypeScript 総合プロジェクト実践

本チュートリアルでは、完全なプロジェクト事例を通して、TypeScript のさまざまな機能を総合的に活用します。

プロジェクト構築から実際の開発まで、実際のプロジェクトにおける TypeScript の応用を完全に示します。


SVG 図: プロジェクトアーキテクチャ 背景 タイトル プロジェクト実践: タスク管理システム フロントエンド フロントエンド (React + TS) Vite + React 18 コンポーネント + Hooks 型安全なUI 矢印 バックエンド バックエンド (Node.js) Express + TS RESTful API 型定義 矢印 データ データストレージ In-Memory (デモ) 下半分: 機能 プロジェクトの機能特性 機能1 タスク CRUD 操作 機能2 型安全な API 機能3 状態管理 矢印マーク

なぜ総合プロジェクト実践が必要か

TypeScript の構文を学んだ後、実際のプロジェクトを通して知識を定着させる必要があります。

本チュートリアルでは、フロントエンド、バックエンド、型定義を網羅した完全なタスク管理システムを紹介します。

このプロジェクトを通して、実際の開発における TypeScript のベストプラクティスを習得できます。

プロジェクト目標:タスクの作成、検索、更新、削除機能を含むタスク管理システムを作成します。


プロジェクト構造

Monorepo スタイルでプロジェクト構造を整理します。

ディレクトリ構造

task-manager/
├── src/
│   ├── types/# 型定義
│   │   ├── task.ts# タスク型
│   │   ├── api.ts# API 型
│   │   └── index.ts# 型エクスポート
│   │
│   ├── services/# サービス層
│   │   ├── taskService.ts# タスクサービス
│   │   └── index.ts
│   │
│   ├── components/# React コンポーネント
│   │   ├── TaskList.tsx# タスクリスト
│   │   ├── TaskItem.tsx# タスク項目
│   │   ├── TaskForm.tsx# タスクフォーム
│   │   └── index.ts
│   │
│   ├── hooks/# カスタムHooks
│   │   ├── useTasks.ts# タスク状態管理
│   │   └── index.ts
│   │
│   ├── App.tsx# メインアプリケーション
│   ├── App.css# スタイル
│   └── main.tsx# エントリファイル
│
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

ディレクトリ区分:機能ごとにディレクトリを分け、型定義、サービス層、コンポーネントを分離します。


型定義

まずプロジェクトのコア型を定義します。

src/types/task.ts

// タスク状態の列挙型
export type TaskStatus = "pending" | "in-progress" | "completed";

// タスク優先度の列挙型
export type TaskPriority = "low" | "medium" | "high";

// タスクインターフェース定義
export interface Task {
    id: string;               // タスクID
    title: string;            // タスクタイトル
    description?: string;      // タスク説明(任意)
    status: TaskStatus;       // タスク状態
    priority: TaskPriority;   // タスク優先度
    createdAt: string;        // 作成日時
    updatedAt: string;         // 更新日時
    dueDate?: string;         // 期限(任意)
    tags?: string[];          // タグ(任意)
}

// タスク作成の入力型
export interface CreateTaskInput {
    title: string;
    description?: string;
    priority: TaskPriority;
    dueDate?: string;
    tags?: string[];
}

// タスク更新の入力型
export interface UpdateTaskInput {
    title?: string;
    description?: string;
    status?: TaskStatus;
    priority?: TaskPriority;
    dueDate?: string;
    tags?: string[];
}

// タスクフィルタオプション
export interface TaskFilter {
    status?: TaskStatus;
    priority?: TaskPriority;
    search?: string;
}

型の階層化:入力型、出力型、フィルタ型を分けて定義し、保守しやすくします。


API型定義

API 関連の型を定義します。

< h2 class="example">src/types/api.ts
// 汎用 API レスポンス型
export interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
    message?: string;
}

// ページネーションメタデータ
export interface PaginationMeta {
    total: number;
    page: number;
    pageSize: number;
    totalPages: number;
}

// ページネーションレスポンス型
export interface PaginatedResponse<T> {
    items: T[];
    meta: PaginationMeta;
}

// リクエストエラー型
export interface ApiError {
    code: string;
    message: string;
    details?: Record<string, string>;
}

// HTTP メソッド型
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// タスク関連の API エンドポイント
export interface TaskEndpoints {
    getAll: "/api/tasks";
    getById: "/api/tasks/:id";
    create: "/api/tasks";
    update: "/api/tasks/:id";
    delete: "/api/tasks/:id";
}

API 型:統一されたレスポンス形式とエラー処理型により、フロントエンドとバックエンドの連携が容易になります。


タスクサービス層

タスク管理のビジネスロジックを実装します。

src/services/taskService.ts

// 型定義をインポート
import {
    Task,
    CreateTaskInput,
    UpdateTaskInput,
    TaskFilter,
    TaskStatus,
    TaskPriority
} from "../types/task";

// 一意のIDを生成
function generateId(): string {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

// データベースを模擬(メモリストレージ)
let tasks: Task[] = [
    {
        id: "1",
        title: "TypeScript を学ぶ",
        description: "TypeScript の基礎と高度な機能を習得",
        status: "completed",
        priority: "high",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        tags: ["学習", "TypeScript"]
    },
    {
        id: "2",
        title: "タスク管理システムを開発",
        description: "React + TypeScript を使用して開発",
        status: "in-progress",
        priority: "high",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        tags: ["プロジェクト", "実践"]
    }
];

// タスクサービスクラス
class TaskService {
    // すべてのタスクを取得
    getAll(filter?: TaskFilter): Task[] {
        let result = [...tasks];

        if (filter) {
            if (filter.status) {
                result = result.filter(t => t.status === filter.status);
            }
            if (filter.priority) {
                result = result.filter(t => t.priority === filter.priority);
            }
            if (filter.search) {
                const search = filter.search.toLowerCase();
                result = result.filter(t =>
                    t.title.toLowerCase().includes(search) ||
                    t.description?.toLowerCase().includes(search)
                );
            }
        }

        return result;
    }

    // IDでタスクを取得
    getById(id: string): Task | undefined {
        return tasks.find(t => t.id === id);
    }

    // タスクを作成
    create(input: CreateTaskInput): Task {
        const now = new Date().toISOString();
        const task: Task = {
            id: generateId(),
            title: input.title,
            description: input.description,
            status: "pending",
            priority: input.priority,
            createdAt: now,
            updatedAt: now,
            dueDate: input.dueDate,
            tags: input.tags
        };

        tasks.push(task);
        return task;
    }

    // タスクを更新
    update(id: string, input: UpdateTaskInput): Task | null {
        const index = tasks.findIndex(t => t.id === id);
        if (index === -1) return null;

        const task = tasks[index];
        const updated: Task = {
            ...task,
            ...input,
            updatedAt: new Date().toISOString()
        };

        tasks[index] = updated;
        return updated;
    }

    // タスクを削除
    delete(id: string): boolean {
        const index = tasks.findIndex(t => t.id === id);
        if (index === -1) return false;

        tasks.splice(index, 1);
        return true;
    }

    // タスク状態を更新
    updateStatus(id: string, status: TaskStatus): Task | null {
        return this.update(id, { status });
    }
}

// サービスインスタンスをエクスポート
export const taskService = new TaskService();

サービス層:ビジネスロジックをサービス層に集中させ、テストと保守を容易にします。


カスタムHook

Hook を使用してタスク状態を管理します。

< h2 class="example">src/hooks/useTasks.ts
// React Hooks と型をインポート
import { useState, useEffect, useCallback } from "react";
import {
    Task,
    CreateTaskInput,
    UpdateTaskInput,
    TaskFilter,
    TaskStatus,
    TaskPriority
} from "../types/task";
import { taskService } from "../services/taskService";

// Hook が返す状態型
interface UseTasksReturn {
    tasks: Task[];
    loading: boolean;
    error: string | null;
    filter: TaskFilter;
    // 操作メソッド
    createTask: (input: CreateTaskInput) => Promise<void>;
    updateTask: (id: string, input: UpdateTaskInput) => Promise<void>;
    deleteTask: (id: string) => Promise<void>;
    updateStatus: (id: string, status: TaskStatus) => Promise<void>;
    setFilter: (filter: TaskFilter) => void;
    refresh: () => void;
}

// デフォルトフィルタを初期化
const defaultFilter: TaskFilter = {};

export function useTasks(): UseTasksReturn {
    // タスクリスト状態
    const [tasks, setTasks] = useState<Task[]>([]);
    // ロード状態
    const [loading, setLoading] = useState(true);
    // エラー状態
    const [error, setError] = useState<string | null>(null);
    // フィルタ条件
    const [filter, setFilter] = useState<TaskFilter>(defaultFilter);

    // タスクリストをロード
    const loadTasks = useCallback(() => {
        setLoading(true);
        setError(null);

        try {
            const data = taskService.getAll(filter);
            setTasks(data);
        } catch (err) {
            setError(err instanceof Error ? err.message : "ロードに失敗しました");
        } finally {
            setLoading(false);
        }
    }, [filter]);

    // 初期ロードとフィルタ変更時に再ロード
    useEffect(() => {
        loadTasks();
    }, [loadTasks]);

    // タスクを作成
    const createTask = useCallback(async (input: CreateTaskInput) => {
        try {
            taskService.create(input);
            loadTasks();
        } catch (err) {
            setError(err instanceof Error ? err.message : "作成に失敗しました");
        }
    }, [loadTasks]);

    // タスクを更新
    const updateTask = useCallback(async (id: string, input: UpdateTaskInput) => {
        try {
            taskService.update(id, input);
            loadTasks();
        } catch (err) {
            setError(err instanceof Error ? err.message : "更新に失敗しました");
        }
    }, [loadTasks]);

    // タスクを削除
    const deleteTask = useCallback(async (id: string) => {
        try {
            taskService.delete(id);
            loadTasks();
        } catch (err) {
            setError(err instanceof Error ? err.message : "削除に失敗しました");
        }
    }, [loadTasks]);

    // タスク状態を更新
    const updateStatus = useCallback(async (id: string, status: TaskStatus) => {
        try {
            taskService.updateStatus(id, status);
            loadTasks();
        } catch (err) {
            setError(err instanceof Error ? err.message : "状態の更新に失敗しました");
        }
    }, [loadTasks]);

    // タスクリストを更新
    const refresh = useCallback(() => {
        loadTasks();
    }, [loadTasks]);

    return {
        tasks,
        loading,
        error,
        filter,
        createTask,
        updateTask,
        deleteTask,
        updateStatus,
        setFilter,
        refresh
    };
}

カスタムHook:状態管理とビジネスロジックを Hook にカプセル化することで、コンポーネントがより簡潔になります。


React コンポーネント

Hook を使用してタスクリストコンポーネントを作成します。

src/components/TaskList.tsx

// React とカスタムHookをインポート
import React from "react";
import { useTasks } from "../hooks/useTasks";
import { Task, TaskStatus, TaskPriority } from "../types/task";

// タスク項目コンポーネントの Props
interface TaskItemProps {
    task: Task;
    onStatusChange: (id: string, status: TaskStatus) => void;
    onDelete: (id: string) => void;
}

// タスク項目コンポーネント
const TaskItem: React.FC<TaskItemProps> = ({
    task,
    onStatusChange,
    onDelete
}) => {
    // 状態スタイルマッピング
    const statusStyles: Record<TaskStatus, string> = {
        "pending": "status-pending",
        "in-progress": "status-progress",
        "completed": "status-completed"
    };

    // 優先度スタイルマッピング
    const priorityLabels: Record<TaskPriority, string> = {
        "low": "低",
        "medium": "中",
        "high": "高"
    };

    return (
        <div className={`task-item ${statusStyles[task.status]}`}>
            <div className="task-content">
                <h3 className="task-title">{task.title}</h3>
                {task.description && (
                    <p className="task-description">{task.description}</p>
                )}
                <div className="task-meta">
                    <span className={`priority priority-${task.priority}`}>
                        {priorityLabels[task.priority]}
                    </span>
                    <span className="task-date">
                        {new Date(task.createdAt).toLocaleDateString()}
                    </span>
                </div>
            </div>
            <div className="task-actions">
                <select
                    value={task.status}
                    onChange={(e) => onStatusChange(task.id, e.target.value as TaskStatus)}
                    className="status-select"
                >
                    <option value="pending">未処理</option>
                    <option value="in-progress">進行中</option>
                    <option value="completed">完了済み</option>
                </select>
                <button
                    onClick={() => onDelete(task.id)}
                    className="delete-btn"
                >
削除
                </button>
            </div>
        </div>
    );
};

// タスクリストコンポーネント
export const TaskList: React.FC = () => {
    // カスタムHookを使用してタスク状態を取得
    const {
        tasks,
        loading,
        error,
        updateStatus,
        deleteTask
    } = useTasks();

    // ロード状態を描画
    if (loading) {
        return <div className="loading">読み込み中...</div>;
    }

    // エラー状態を描画
    if (error) {
        return <div className="error">エラー: {error}</div>;
    }

    // 空の状態を描画
    if (tasks.length === 0) {
        return (
            <div className="empty">
                <p>タスクはありません</p>
                <p>上のボタンをクリックして新しいタスクを作成</p>
            </div>
        );
    }

    // タスクリストを描画
    return (
        <div className="task-list">
            {tasks.map(task => (
                <TaskItem
                    key={task.id}
                    task={task}
                    onStatusChange={updateStatus}
                    onDelete={deleteTask}
                />
            ))}
        </div>
    );
};

export default TaskList;

コンポーネント分割:TaskItem を独立したコンポーネントに分割することで、コードがより明確になります。


メインアプリケーションコンポーネント

すべてのコンポーネントを統合して、完全なアプリケーションを構成します。

src/App.tsx

// React と型をインポート
import React, { useState } from "react";
import { TaskList } from "./components/TaskList";
import { useTasks } from "./hooks/useTasks";
import { CreateTaskInput, TaskPriority } from "./types/task";
import "./App.css";

// タスクフォームコンポーネントの Props
interface TaskFormProps {
    onSubmit: (input: CreateTaskInput) => void;
}

// タスクフォームコンポーネント
const TaskForm: React.FC<TaskFormProps> = ({ onSubmit }) => {
    // フォーム状態
    const [title, setTitle] = useState("");
    const [description, setDescription] = useState("");
    const [priority, setPriority] = useState<TaskPriority>("medium");

    // 送信処理
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();

        if (!title.trim()) {
            alert("タスクのタイトルを入力してください");
            return;
        }

        onSubmit({
            title: title.trim(),
            description: description.trim() || undefined,
            priority
        });

        // フォームをリセット
        setTitle("");
        setDescription("");
        setPriority("medium");
    };

    return (
        <form onSubmit={handleSubmit} className="task-form">
            <input
                type="text"
                value={title}
                onChange={(e) => setTitle(e.target.value)}
                placeholder="タスクのタイトルを入力"
                className="form-input"
            />
            <input
                type="text"
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                placeholder="タスクの説明を入力(任意)"
                className="form-input"
            />
            <select
                value={priority}
                onChange={(e) => setPriority(e.target.value as TaskPriority)}
                className="form-select"
            >
                <option value="low">低優先度</option>
                <option value="medium">中優先度</option>
                <option value="high">高優先度</option>
            </select>
            <button type="submit" className="submit-btn">
タスクを追加
            </button>
        </form>
    );
};

// メインアプリケーションコンポーネント
const App: React.FC = () => {
    // カスタムHookを使用
    const { createTask, error } = useTasks();

    // エラーメッセージを描画
    const renderError = () => {
        if (!error) return null;
        return <div className="app-error">{error}</div>;
    };

    return (
        <div className="app">
            <header className="app-header">
                <h1>TypeScript タスク管理システム</h1>
                <p>総合実践プロジェクト</p>
            </header>

            {renderError()}

            <main className="app-main">
                <TaskForm onSubmit={createTask} />
                <TaskList />
            </main>

            <footer className="app-footer">
                <p>Powered by TypeScript + React</p>
            </footer>
        </div>
    );
};

export default App;

コンポーネント構成:単純なコンポーネントを組み合わせて複雑な UI を構築します。


プロジェクト設定

プロジェクトのTypeScriptおよびビルド設定。

tsconfig.json

{
    "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "lib": ["ES2020", "DOM", "DOM.Iterable"],
        "module": "ESNext",
        "skipLibCheck": true,

        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react-jsx",

        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
}

厳格モード:strictを有効にして、最も完全な型チェックを実現します。


注意事項

  • 型優先:最初に型を定義し、その後で実装コードを記述します
  • インターフェース vs 型:オブジェクト型にはインターフェース、ユニオン型には型エイリアスを使用します
  • 階層アーキテクチャ:型、サービス、コンポーネントを階層的に整理します
  • Hookのカプセル化:ビジネスロジックをHookにカプセル化します

ベストプラクティス:型定義はTypeScriptプロジェクトの基盤であり、真剣に設計する必要があります。


まとめ

この総合プロジェクトを通じて、TypeScriptの核となる概念を実践しました。

  • 型定義:Task、CreateTaskInput、TaskFilterなどのインターフェース
  • サービス層:TaskServiceがビジネスロジックをカプセル化
  • カスタムHook:useTasksで状態を管理
  • Reactコンポーネント:型安全なコンポーネント開発
  • プロジェクト設定:厳格なTypeScript設定

アドバイス:実際のプロジェクトに積極的に参加し、実践を通じてTypeScriptへの理解を深めてください。

その他の拡張