====== 第二十章:项目实战 ====== ===== 本章概述 ===== 本章将通过三个完整的实战项目,将前面学习的 TypeScript 知识综合应用到实际开发中。我们将分别构建一个 React + TypeScript 的前端应用、一个 Node.js + TypeScript 的后端 API 服务,以及总结大型项目的最佳实践。通过这些实战案例,你将掌握 TypeScript 在真实项目中的开发技巧。 ===== 20.1 React + TypeScript 实战 ===== ==== 20.1.1 项目初始化 ==== 使用 Vite 创建 React + TypeScript 项目: npm create vite@latest todo-app -- --template react-ts cd todo-app npm install 项目结构: src/ ├── components/ # UI 组件 │ ├── TodoItem.tsx │ └── TodoList.tsx ├── hooks/ # 自定义 Hooks │ └── useTodos.ts ├── types/ # 类型定义 │ └── index.ts ├── utils/ # 工具函数 │ └── storage.ts ├── App.tsx └── main.tsx //tsx指ts文件支持jsx语法,类似"
Hello
",等效于"React.createElement("div", null, "Hello")" ==== 20.1.2 类型定义设计 ==== // src/types/index.ts // 待办事项状态 export type TodoStatus = "pending" | "in-progress" | "completed"; // 待办事项优先级 export type TodoPriority = "low" | "medium" | "high"; // 待办事项接口 export interface Todo { id: string; title: string; description?: string; status: TodoStatus; priority: TodoPriority; createdAt: Date; updatedAt: Date; dueDate?: Date; tags: string[]; } // 创建待办事项的请求类型 type OptionalTodoFields = "id" | "createdAt" | "updatedAt" | "status"; export type CreateTodoDTO = Omit & Partial>; // 更新待办事项的请求类型 export type UpdateTodoDTO = Partial>; // 过滤器类型 export interface TodoFilters { status?: TodoStatus; priority?: TodoPriority; searchQuery?: string; tags?: string[]; } // 排序选项 export type TodoSortField = "createdAt" | "dueDate" | "priority"; export type TodoSortOrder = "asc" | "desc"; export interface TodoSortOptions { field: TodoSortField; order: TodoSortOrder; } // API 响应类型 export interface ApiResponse { success: boolean; data: T; message?: string; } export interface ApiError { success: false; error: { code: string; message: string; details?: Record; }; } // 组件 Props 类型 export interface TodoItemProps { todo: Todo; onToggle: (id: string) => void; onDelete: (id: string) => void; onEdit: (todo: Todo) => void; } export interface TodoListProps { todos: Todo[]; filters: TodoFilters; sortOptions: TodoSortOptions; onToggle: (id: string) => void; onDelete: (id: string) => void; onEdit: (todo: Todo) => void; } ==== 20.1.3 自定义 Hooks ==== // src/hooks/useTodos.ts import { useState, useEffect, useCallback, useMemo } from "react"; import type { Todo, CreateTodoDTO, UpdateTodoDTO, TodoFilters, TodoSortOptions } from "../types"; import { storage } from "../utils/storage"; const STORAGE_KEY = "todos"; // 自定义 Hook 返回类型 interface UseTodosReturn { todos: Todo[]; isLoading: boolean; error: Error | null; addTodo: (dto: CreateTodoDTO) => void; updateTodo: (id: string, dto: UpdateTodoDTO) => void; deleteTodo: (id: string) => void; toggleTodo: (id: string) => void; filteredTodos: Todo[]; setFilters: React.Dispatch>; setSortOptions: React.Dispatch>; } export function useTodos(): UseTodosReturn { const [todos, setTodos] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [filters, setFilters] = useState({}); const [sortOptions, setSortOptions] = useState({ field: "createdAt", order: "desc" }); // 加载数据 useEffect(() => { try { const stored = storage.getItem(STORAGE_KEY); if (stored) { // 恢复 Date 对象 const parsed = stored.map(todo => ({ ...todo, createdAt: new Date(todo.createdAt), updatedAt: new Date(todo.updatedAt), dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined })); setTodos(parsed); } } catch (err) { setError(err instanceof Error ? err : new Error("Failed to load todos")); } finally { setIsLoading(false); } }, []); // 保存数据 useEffect(() => { if (!isLoading) { storage.setItem(STORAGE_KEY, todos); } }, [todos, isLoading]); // 生成唯一 ID const generateId = (): string => { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }; // 添加待办事项 const addTodo = useCallback((dto: CreateTodoDTO): void => { const now = new Date(); const newTodo: Todo = { id: generateId(), status: "pending", createdAt: now, updatedAt: now, ...dto }; setTodos(prev => [newTodo, ...prev]); }, []); // 更新待办事项 const updateTodo = useCallback((id: string, dto: UpdateTodoDTO): void => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, ...dto, updatedAt: new Date() } : todo )); }, []); // 删除待办事项 const deleteTodo = useCallback((id: string): void => { setTodos(prev => prev.filter(todo => todo.id !== id)); }, []); // 切换完成状态 const toggleTodo = useCallback((id: string): void => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, status: todo.status === "completed" ? "pending" : "completed" as TodoStatus, updatedAt: new Date() } : todo )); }, []); // 过滤和排序 const filteredTodos = useMemo(() => { let result = [...todos]; // 应用过滤器 if (filters.status) { result = result.filter(todo => todo.status === filters.status); } if (filters.priority) { result = result.filter(todo => todo.priority === filters.priority); } if (filters.searchQuery) { const query = filters.searchQuery.toLowerCase(); result = result.filter(todo => todo.title.toLowerCase().includes(query) || todo.description?.toLowerCase().includes(query) ); } if (filters.tags && filters.tags.length > 0) { result = result.filter(todo => filters.tags!.some(tag => todo.tags.includes(tag)) ); } // 应用排序 result.sort((a, b) => { let comparison = 0; switch (sortOptions.field) { case "priority": const priorityMap = { high: 3, medium: 2, low: 1 }; comparison = priorityMap[a.priority] - priorityMap[b.priority]; break; case "dueDate": if (!a.dueDate && !b.dueDate) comparison = 0; else if (!a.dueDate) comparison = 1; else if (!b.dueDate) comparison = -1; else comparison = a.dueDate.getTime() - b.dueDate.getTime(); break; case "createdAt": default: comparison = a.createdAt.getTime() - b.createdAt.getTime(); } return sortOptions.order === "asc" ? comparison : -comparison; }); return result; }, [todos, filters, sortOptions]); return { todos, isLoading, error, addTodo, updateTodo, deleteTodo, toggleTodo, filteredTodos, setFilters, setSortOptions }; } ==== 20.1.4 类型安全的组件 ==== // src/components/TodoItem.tsx import React, { useState } from "react"; import type { Todo, TodoItemProps, TodoPriority, TodoStatus } from "../types"; // 优先级样式映射 const priorityStyles: Record = { low: "bg-green-100 text-green-800", medium: "bg-yellow-100 text-yellow-800", high: "bg-red-100 text-red-800" }; // 状态标签映射 const statusLabels: Record = { pending: "待处理", "in-progress": "进行中", completed: "已完成" }; export const TodoItem: React.FC = ({ todo, onToggle, onDelete, onEdit }) => { const [isEditing, setIsEditing] = useState(false); const [editForm, setEditForm] = useState({ title: todo.title, description: todo.description || "", priority: todo.priority }); const handleSave = (): void => { onEdit({ ...todo, title: editForm.title, description: editForm.description || undefined, priority: editForm.priority }); setIsEditing(false); }; if (isEditing) { return (
setEditForm(prev => ({ ...prev, title: e.target.value }))} className="w-full p-2 border rounded mb-2" />