TypeScript 総合プロジェクト実践
本チュートリアルでは、完全なプロジェクト事例を通して、TypeScript のさまざまな機能を総合的に活用します。
プロジェクト構築から実際の開発まで、実際のプロジェクトにおける TypeScript の応用を完全に示します。
SVG 図: プロジェクトアーキテクチャ
なぜ総合プロジェクト実践が必要か
TypeScript の構文を学んだ後、実際のプロジェクトを通して知識を定着させる必要があります。
本チュートリアルでは、フロントエンド、バックエンド、型定義を網羅した完全なタスク管理システムを紹介します。
このプロジェクトを通して、実際の開発における TypeScript のベストプラクティスを習得できます。
プロジェクト目標:タスクの作成、検索、更新、削除機能を含むタスク管理システムを作成します。
プロジェクト構造
Monorepo スタイルでプロジェクト構造を整理します。
ディレクトリ構造
├── 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 関連の型を定義します。
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 を使用してタスク状態を管理します。
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
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
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への理解を深めてください。