这是本文档旧的修订版!
第二十章:项目实战
本章概述
本章将通过三个完整的实战项目,将前面学习的 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语法,类似"<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; }
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<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
};
}
20.1.4 类型安全的组件
// 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>
);
};
20.1.5 泛型组件示例
// 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)}
/>
);
}
20.2 Node.js + TypeScript 实战
20.2.1 项目初始化
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 ); } }
20.2.3 控制器实现
// 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();
20.2.4 中间件实现
// 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
});
}
20.3 大型项目最佳实践
20.3.1 项目结构规范
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
20.3.2 类型组织策略
// 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;
20.3.3 严格类型检查配置
{
"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>
);
}
参考答案
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>
);
}
练习 2:API 类型安全
设计一个类型安全的 HTTP 客户端:
// 要求: // 1. 支持泛型请求/响应 // 2. 自动推断返回类型 // 3. 类型安全的错误处理 // 4. 支持请求/响应拦截器
参考答案
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); // 类型安全