typescript:第二十章_项目实战

第二十章:项目实战

本章将通过三个完整的实战项目,将前面学习的 TypeScript 知识综合应用到实际开发中。我们将分别构建一个 React + TypeScript 的前端应用、一个 Node.js + TypeScript 的后端 API 服务,以及总结大型项目的最佳实践。通过这些实战案例,你将掌握 TypeScript 在真实项目中的开发技巧。

使用 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;
}
// 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
  };
}
// 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>
  );
};
// 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)}
    />
  );
}
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
    );
  }
}
// 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();
// 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
  });
}
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
// 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;
{
  "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>
  );
}

设计一个类型安全的 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);  // 类型安全

该主题尚不存在

您访问的页面并不存在。如果允许,您可以使用创建该页面按钮来创建它。

  • typescript/第二十章_项目实战.txt
  • 最后更改: 2026/03/09 15:59
  • 张叶安