显示页面讨论过去修订反向链接回到顶部 本页面只读。您可以查看源文件,但不能更改它。如果您觉得这是系统错误,请联系管理员。 ====== 第二十章:项目实战 ====== ===== 本章概述 ===== 本章将通过三个完整的实战项目,将前面学习的 TypeScript 知识综合应用到实际开发中。我们将分别构建一个 React + TypeScript 的前端应用、一个 Node.js + TypeScript 的后端 API 服务,以及总结大型项目的最佳实践。通过这些实战案例,你将掌握 TypeScript 在真实项目中的开发技巧。 ===== 20.1 React + TypeScript 实战 ===== ==== 20.1.1 项目初始化 ==== 使用 Vite 创建 React + TypeScript 项目: <code bash> 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语法,类似"<div>Hello</div>",等效于"React.createElement("div", null, "Hello")" ==== 20.1.2 类型定义设计 ==== <code typescript> // 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<Todo, OptionalTodoFields> & Partial<Pick<Todo, "status">>; // 更新待办事项的请求类型 export type UpdateTodoDTO = Partial<Omit<Todo, "id" | "createdAt">>; // 过滤器类型 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<T> { success: boolean; data: T; message?: string; } export interface ApiError { success: false; error: { code: string; message: string; details?: Record<string, string[]>; }; } // 组件 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; } </code> ==== 20.1.3 自定义 Hooks ==== <code typescript> // 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<React.SetStateAction<TodoFilters>>; setSortOptions: React.Dispatch<React.SetStateAction<TodoSortOptions>>; } export function useTodos(): UseTodosReturn { const [todos, setTodos] = useState<Todo[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [filters, setFilters] = useState<TodoFilters>({}); const [sortOptions, setSortOptions] = useState<TodoSortOptions>({ field: "createdAt", order: "desc" }); // 加载数据 useEffect(() => { try { const stored = storage.getItem<Todo[]>(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 }; } </code> ==== 20.1.4 类型安全的组件 ==== <code typescript> // src/components/TodoItem.tsx import React, { useState } from "react"; import type { Todo, TodoItemProps, TodoPriority, TodoStatus } from "../types"; // 优先级样式映射 const priorityStyles: Record<TodoPriority, string> = { low: "bg-green-100 text-green-800", medium: "bg-yellow-100 text-yellow-800", high: "bg-red-100 text-red-800" }; // 状态标签映射 const statusLabels: Record<TodoStatus, string> = { pending: "待处理", "in-progress": "进行中", completed: "已完成" }; export const TodoItem: React.FC<TodoItemProps> = ({ 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 ( <div className="p-4 border rounded-lg bg-gray-50"> <input type="text" value={editForm.title} onChange={(e) => setEditForm(prev => ({ ...prev, title: e.target.value }))} className="w-full p-2 border rounded mb-2" /> <textarea value={editForm.description} onChange={(e) => setEditForm(prev => ({ ...prev, description: e.target.value }))} className="w-full p-2 border rounded mb-2" rows={2} /> <select value={editForm.priority} onChange={(e) => setEditForm(prev => ({ ...prev, priority: e.target.value as TodoPriority }))} className="p-2 border rounded mb-2" > <option value="low">低优先级</option> <option value="medium">中优先级</option> <option value="high">高优先级</option> </select> <div className="flex gap-2"> <button onClick={handleSave} className="px-4 py-2 bg-blue-500 text-white rounded" > 保存 </button> <button onClick={() => setIsEditing(false)} className="px-4 py-2 bg-gray-300 rounded" > 取消 </button> </div> </div> ); } return ( <div className={`p-4 border rounded-lg ${ todo.status === "completed" ? "bg-gray-100 opacity-75" : "bg-white" }`}> <div className="flex items-start justify-between"> <div className="flex items-start gap-3"> <input type="checkbox" checked={todo.status === "completed"} onChange={() => onToggle(todo.id)} className="mt-1" /> <div> <h3 className={`font-semibold ${ todo.status === "completed" ? "line-through text-gray-500" : "" }`}> {todo.title} </h3> {todo.description && ( <p className="text-gray-600 text-sm mt-1">{todo.description}</p> )} <div className="flex gap-2 mt-2"> <span className={`text-xs px-2 py-1 rounded ${priorityStyles[todo.priority]}`}> {todo.priority === "low" ? "低" : todo.priority === "medium" ? "中" : "高"} </span> <span className="text-xs px-2 py-1 rounded bg-gray-100"> {statusLabels[todo.status]} </span> {todo.dueDate && ( <span className="text-xs text-gray-500"> 截止: {todo.dueDate.toLocaleDateString()} </span> )} </div> </div> </div> <div className="flex gap-2"> <button onClick={() => setIsEditing(true)} className="text-blue-500 hover:text-blue-700" > 编辑 </button> <button onClick={() => onDelete(todo.id)} className="text-red-500 hover:text-red-700" > 删除 </button> </div> </div> </div> ); }; </code> ==== 20.1.5 泛型组件示例 ==== <code typescript> // src/components/DataTable.tsx import React from "react"; // 表格列定义 interface Column<T, K extends keyof T = keyof T> { key: K; title: string; render?: (value: T[K], item: T) => React.ReactNode; width?: string; } // 表格 Props interface DataTableProps<T extends Record<string, any>> { data: T[]; columns: Column<T>[]; keyExtractor: (item: T) => string; onRowClick?: (item: T) => void; loading?: boolean; emptyMessage?: string; } // 泛型组件 export function DataTable<T extends Record<string, any>>({ data, columns, keyExtractor, onRowClick, loading = false, emptyMessage = "暂无数据" }: DataTableProps<T>): JSX.Element { if (loading) { return <div className="text-center py-8">加载中...</div>; } if (data.length === 0) { return <div className="text-center py-8 text-gray-500">{emptyMessage}</div>; } return ( <table className="w-full border-collapse"> <thead> <tr className="bg-gray-100"> {columns.map(col => ( <th key={String(col.key)} className="p-3 text-left border-b" style={{ width: col.width }} > {col.title} </th> ))} </tr> </thead> <tbody> {data.map(item => ( <tr key={keyExtractor(item)} className="border-b hover:bg-gray-50 cursor-pointer" onClick={() => onRowClick?.(item)} > {columns.map(col => ( <td key={String(col.key)} className="p-3"> {col.render ? col.render(item[col.key], item) : String(item[col.key]) } </td> ))} </tr> ))} </tbody> </table> ); } // 使用示例 interface Product { id: string; name: string; price: number; stock: number; } function ProductList() { const products: Product[] = [ { id: "1", name: "笔记本电脑", price: 5999, stock: 10 }, { id: "2", name: "手机", price: 3999, stock: 20 } ]; return ( <DataTable data={products} keyExtractor={p => p.id} columns={[ { key: "name", title: "产品名称" }, { key: "price", title: "价格", render: (value) => `¥${value.toFixed(2)}` }, { key: "stock", title: "库存", render: (value) => ( <span className={value < 10 ? "text-red-500" : ""}> {value} </span> ) } ]} onRowClick={(product) => console.log("Clicked:", product)} /> ); } </code> ===== 20.2 Node.js + TypeScript 实战 ===== ==== 20.2.1 项目初始化 ==== <code bash> mkdir todo-api cd todo-api npm init -y npm install express cors helmet morgan npm install -D typescript @types/node @types/express @types/cors @types/morgan ts-node nodemon npx tsc --init tsconfig.json 配置: <code json> { "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ==== 20.2.2 类型定义和接口 ==== <code typescript> // src/types/index.ts import { Request, Response, NextFunction } from "express"; // 扩展 Express Request 类型 declare global { namespace Express { interface Request { userId?: string; requestId: string; } } } // 环境变量类型 export interface EnvVars { NODE_ENV: "development" | "production" | "test"; PORT: string; DATABASE_URL: string; JWT_SECRET: string; JWT_EXPIRES_IN: string; } // 通用 API 响应类型 export interface ApiSuccessResponse<T> { success: true; data: T; meta?: { page: number; limit: number; total: number; totalPages: number; }; } export interface ApiErrorResponse { success: false; error: { code: string; message: string; details?: Record<string, string[]>; }; requestId: string; } export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse; // 分页参数 export interface PaginationParams { page: number; limit: number; sortBy?: string; sortOrder?: "asc" | "desc"; } // 通用实体接口 export interface BaseEntity { id: string; createdAt: Date; updatedAt: Date; } // 待办事项类型 export type TodoStatus = "pending" | "in-progress" | "completed"; export type TodoPriority = "low" | "medium" | "high"; export interface Todo extends BaseEntity { userId: string; title: string; description?: string; status: TodoStatus; priority: TodoPriority; dueDate?: Date; tags: string[]; } // DTO 类型 export interface CreateTodoDTO { title: string; description?: string; priority?: TodoPriority; dueDate?: string; tags?: string[]; } export interface UpdateTodoDTO { title?: string; description?: string; status?: TodoStatus; priority?: TodoPriority; dueDate?: string; tags?: string[]; } export interface TodoFilters { status?: TodoStatus; priority?: TodoPriority; search?: string; tags?: string[]; dueBefore?: string; dueAfter?: string; } // 中间件类型 export type AsyncRequestHandler = ( req: Request, res: Response, next: NextFunction ) => Promise<any>; // 自定义错误类 export class AppError extends Error { constructor( public code: string, message: string, public statusCode: number = 500, public details?: Record<string, string[]> ) { super(message); this.name = "AppError"; Error.captureStackTrace(this, this.constructor); } } // 验证错误 export class ValidationError extends AppError { constructor(details: Record<string, string[]>) { super("VALIDATION_ERROR", "Validation failed", 400, details); } } // 未找到错误 export class NotFoundError extends AppError { constructor(resource: string, id?: string) { super( "NOT_FOUND", id ? `${resource} with id '${id}' not found` : `${resource} not found`, 404 ); } } </code> ==== 20.2.3 控制器实现 ==== <code typescript> // src/controllers/todo.controller.ts import { Request, Response, NextFunction } from "express"; import { v4 as uuidv4 } from "uuid"; import { Todo, CreateTodoDTO, UpdateTodoDTO, TodoFilters, PaginationParams, ApiSuccessResponse, AppError, NotFoundError, ValidationError } from "../types"; // 模拟数据库 const todos: Map<string, Todo> = new Map(); // 验证 DTO function validateCreateDTO(dto: unknown): CreateTodoDTO { const errors: Record<string, string[]> = {}; const data = dto as Record<string, unknown>; if (!data.title || typeof data.title !== "string") { errors.title = ["Title is required and must be a string"]; } else if (data.title.length < 1 || data.title.length > 200) { errors.title = ["Title must be between 1 and 200 characters"]; } if (data.description !== undefined && typeof data.description !== "string") { errors.description = ["Description must be a string"]; } const validPriorities = ["low", "medium", "high"]; if (data.priority !== undefined && !validPriorities.includes(data.priority as string)) { errors.priority = [`Priority must be one of: ${validPriorities.join(", ")}`]; } if (data.dueDate !== undefined) { const date = new Date(data.dueDate as string); if (isNaN(date.getTime())) { errors.dueDate = ["Due date must be a valid date string"]; } } if (Object.keys(errors).length > 0) { throw new ValidationError(errors); } return { title: data.title as string, description: data.description as string | undefined, priority: (data.priority as Todo["priority"]) || "medium", dueDate: data.dueDate as string | undefined, tags: Array.isArray(data.tags) ? data.tags as string[] : [] }; } export class TodoController { // 获取所有待办事项 async getAll( req: Request<{}, {}, {}, TodoFilters & PaginationParams>, res: Response<ApiSuccessResponse<Todo[]>>, next: NextFunction ): Promise<void> { try { const { page = 1, limit = 10, sortBy = "createdAt", sortOrder = "desc", status, priority, search, tags } = req.query; let result = Array.from(todos.values()).filter( todo => todo.userId === req.userId ); // 应用过滤器 if (status) { result = result.filter(t => t.status === status); } if (priority) { result = result.filter(t => t.priority === priority); } if (search) { const query = search.toLowerCase(); result = result.filter(t => t.title.toLowerCase().includes(query) || t.description?.toLowerCase().includes(query) ); } if (tags && tags.length > 0) { result = result.filter(t => tags.some(tag => t.tags.includes(tag)) ); } // 排序 result.sort((a, b) => { const aVal = a[sortBy as keyof Todo]; const bVal = b[sortBy as keyof Todo]; const comparison = aVal! < bVal! ? -1 : aVal! > bVal! ? 1 : 0; return sortOrder === "asc" ? comparison : -comparison; }); // 分页 const total = result.length; const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedResult = result.slice(startIndex, endIndex); res.json({ success: true, data: paginatedResult, meta: { page: Number(page), limit: Number(limit), total, totalPages: Math.ceil(total / limit) } }); } catch (error) { next(error); } } // 获取单个待办事项 async getById( req: Request<{ id: string }>, res: Response<ApiSuccessResponse<Todo>>, next: NextFunction ): Promise<void> { try { const { id } = req.params; const todo = todos.get(id); if (!todo || todo.userId !== req.userId) { throw new NotFoundError("Todo", id); } res.json({ success: true, data: todo }); } catch (error) { next(error); } } // 创建待办事项 async create( req: Request<{}, {}, CreateTodoDTO>, res: Response<ApiSuccessResponse<Todo>>, next: NextFunction ): Promise<void> { try { const dto = validateCreateDTO(req.body); const now = new Date(); const todo: Todo = { id: uuidv4(), userId: req.userId!, title: dto.title, description: dto.description, status: "pending", priority: dto.priority || "medium", dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined, tags: dto.tags || [], createdAt: now, updatedAt: now }; todos.set(todo.id, todo); res.status(201).json({ success: true, data: todo }); } catch (error) { next(error); } } // 更新待办事项 async update( req: Request<{ id: string }, {}, UpdateTodoDTO>, res: Response<ApiSuccessResponse<Todo>>, next: NextFunction ): Promise<void> { try { const { id } = req.params; const existing = todos.get(id); if (!existing || existing.userId !== req.userId) { throw new NotFoundError("Todo", id); } const updates = req.body; const updated: Todo = { ...existing, ...updates, id: existing.id, userId: existing.userId, createdAt: existing.createdAt, updatedAt: new Date() }; todos.set(id, updated); res.json({ success: true, data: updated }); } catch (error) { next(error); } } // 删除待办事项 async delete( req: Request<{ id: string }>, res: Response<ApiSuccessResponse<null>>, next: NextFunction ): Promise<void> { try { const { id } = req.params; const todo = todos.get(id); if (!todo || todo.userId !== req.userId) { throw new NotFoundError("Todo", id); } todos.delete(id); res.json({ success: true, data: null }); } catch (error) { next(error); } } } export const todoController = new TodoController(); </code> ==== 20.2.4 中间件实现 ==== <code typescript> // src/middleware/errorHandler.ts import { Request, Response, NextFunction } from "express"; import { AppError, ApiErrorResponse } from "../types"; export function errorHandler( err: Error, req: Request, res: Response<ApiErrorResponse>, _next: NextFunction ): void { const requestId = req.requestId; if (err instanceof AppError) { res.status(err.statusCode).json({ success: false, error: { code: err.code, message: err.message, details: err.details }, requestId }); return; } // 未知错误 console.error("Unhandled error:", err); res.status(500).json({ success: false, error: { code: "INTERNAL_ERROR", message: process.env.NODE_ENV === "production" ? "An internal error occurred" : err.message }, requestId }); } </code> ===== 20.3 大型项目最佳实践 ===== ==== 20.3.1 项目结构规范 ==== <code> project/ ├── src/ │ ├── modules/ # 按功能模块组织 │ │ ├── users/ │ │ │ ├── types.ts │ │ │ ├── service.ts │ │ │ ├── controller.ts │ │ │ ├── routes.ts │ │ │ ├── validators.ts │ │ │ └── index.ts │ │ └── todos/ │ │ └── ... │ ├── shared/ # 共享资源 │ │ ├── types/ │ │ ├── utils/ │ │ ├── middleware/ │ │ └── constants/ │ ├── config/ # 配置文件 │ ├── infrastructure/ # 基础设施 │ │ ├── database/ │ │ ├── cache/ │ │ └── logger/ │ └── app.ts ├── tests/ │ ├── unit/ │ ├── integration/ │ └── e2e/ ├── prisma/ # ORM 配置 ├── docker-compose.yml └── package.json </code> ==== 20.3.2 类型组织策略 ==== <code typescript> // types/index.ts - 集中导出所有类型 // 基础类型 export * from "./base"; // 模块类型 export * from "../modules/users/types"; export * from "../modules/todos/types"; // API 类型 export * from "./api"; // 通用工具类型 export type Nullable<T> = T | null; export type Optional<T> = T | undefined; export type Maybe<T> = T | null | undefined; export type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R> ? R : never; </code> ==== 20.3.3 严格类型检查配置 ==== <code json> { "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true } } ==== 20.3.4 代码规范与 ESLint ==== <code json> // .eslintrc.json { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" }, "rules": { "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/prefer-nullish-coalescing": "error", "@typescript-eslint/prefer-optional-chain": "error" } } ===== 20.4 本章小结 ===== 本章我们学习了: 1. **React + TypeScript** - 组件类型、Hooks 类型、泛型组件 2. **Node.js + TypeScript** - Express 类型、API 设计、错误处理 3. **项目结构** - 模块化组织、类型分层 4. **最佳实践** - 严格类型检查、代码规范、类型安全 ===== 20.5 练习题 ===== ==== 练习 1:React 组件优化 ==== 优化以下组件的类型定义: <code typescript> // 原始代码 function UserList({ users, onSelect }) { return ( <ul> {users.map(user => ( <li onClick={() => onSelect(user)}>{user.name}</li> ))} </ul> ); } </code> 参考答案 <code typescript> interface User { id: string; name: string; email: string; } interface UserListProps { users: User[]; onSelect: (user: User) => void; } const UserList: React.FC<UserListProps> = ({ users, onSelect }) => { return ( <ul> {users.map(user => ( <li key={user.id} onClick={() => onSelect(user)}> {user.name} </li> ))} </ul> ); }; // 或使用泛型实现通用列表组件 interface ListProps<T> { items: T[]; keyExtractor: (item: T) => string; renderItem: (item: T) => React.ReactNode; onItemPress?: (item: T) => void; } function List<T>({ items, keyExtractor, renderItem, onItemPress }: ListProps<T>) { return ( <ul> {items.map(item => ( <li key={keyExtractor(item)} onClick={() => onItemPress?.(item)} > {renderItem(item)} </li> ))} </ul> ); } </code> ==== 练习 2:API 类型安全 ==== 设计一个类型安全的 HTTP 客户端: <code typescript> // 要求: // 1. 支持泛型请求/响应 // 2. 自动推断返回类型 // 3. 类型安全的错误处理 // 4. 支持请求/响应拦截器 </code> 参考答案 <code typescript> interface ApiResponse<T> { success: true; data: T; } interface ApiError { success: false; error: { code: string; message: string; }; } type ApiResult<T> = ApiResponse<T> | ApiError; class TypedHttpClient { private baseURL: string; constructor(baseURL: string) { this.baseURL = baseURL; } async get<T>(path: string, params?: Record<string, string>): Promise<T> { const url = new URL(path, this.baseURL); if (params) { Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); } const response = await fetch(url.toString()); const result = await response.json() as ApiResult<T>; if (!result.success) { throw new Error(result.error.message); } return result.data; } async post<T, D = unknown>(path: string, data: D): Promise<T> { const response = await fetch(`${this.baseURL}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); const result = await response.json() as ApiResult<T>; if (!result.success) { throw new Error(result.error.message); } return result.data; } } // 使用 interface User { id: number; name: string; } const client = new TypedHttpClient("https://api.example.com"); // 自动推断返回类型 const user = await client.get<User>("/users/1"); console.log(user.name); // 类型安全 </code> ===== 扩展阅读 ===== - [[https://react-typescript-cheatsheet.netlify.app/|React TypeScript Cheatsheet]] - [[https://github.com/typescript-eslint/typescript-eslint|TypeScript ESLint]] - [[https://github.com/sindresorhus/type-fest|type-fest: 高级工具类型]] 登录 Detach Close 该主题尚不存在 您访问的页面并不存在。如果允许,您可以使用创建该页面按钮来创建它。 typescript/第二十章_项目实战.txt 最后更改: 2026/03/09 15:59由 张叶安 登录