强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

TypeScript 开发指南 / 25 - 实战项目

实战项目

本章通过三个完整的实战项目,展示 TypeScript 在不同场景下的应用。

项目一:全栈待办应用

技术栈

技术
前端React + TypeScript + Vite
后端Express + TypeScript
数据库Prisma + SQLite
测试Jest + Testing Library

项目结构

todo-app/
├── client/                  # 前端
│   ├── src/
│   │   ├── components/
│   │   │   ├── TodoList.tsx
│   │   │   ├── TodoItem.tsx
│   │   │   └── AddTodo.tsx
│   │   ├── hooks/
│   │   │   └── useTodos.ts
│   │   ├── types/
│   │   │   └── index.ts
│   │   ├── api/
│   │   │   └── todos.ts
│   │   └── App.tsx
│   ├── tsconfig.json
│   └── vite.config.ts
├── server/                  # 后端
│   ├── src/
│   │   ├── routes/
│   │   │   └── todos.ts
│   │   ├── middleware/
│   │   │   └── errorHandler.ts
│   │   ├── services/
│   │   │   └── todoService.ts
│   │   └── index.ts
│   ├── prisma/
│   │   └── schema.prisma
│   └── tsconfig.json
└── shared/                  # 共享类型
    └── types.ts

共享类型定义

// shared/types.ts
export interface Todo {
  id: number;
  title: string;
  description: string | null;
  completed: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface CreateTodoDto {
  title: string;
  description?: string;
}

export interface UpdateTodoDto {
  title?: string;
  description?: string;
  completed?: boolean;
}

export interface ApiResponse<T> {
  data: T;
  message?: string;
}

export interface ApiError {
  error: string;
  details?: Record<string, string[]>;
}

后端实现

// server/src/services/todoService.ts
import { PrismaClient } from "@prisma/client";
import type { Todo, CreateTodoDto, UpdateTodoDto } from "@shared/types";

const prisma = new PrismaClient();

export class TodoService {
  async findAll(): Promise<Todo[]> {
    return prisma.todo.findMany({
      orderBy: { createdAt: "desc" }
    });
  }

  async findById(id: number): Promise<Todo | null> {
    return prisma.todo.findUnique({ where: { id } });
  }

  async create(data: CreateTodoDto): Promise<Todo> {
    return prisma.todo.create({
      data: {
        title: data.title,
        description: data.description ?? null
      }
    });
  }

  async update(id: number, data: UpdateTodoDto): Promise<Todo> {
    return prisma.todo.update({
      where: { id },
      data
    });
  }

  async delete(id: number): Promise<void> {
    await prisma.todo.delete({ where: { id } });
  }
}
// server/src/routes/todos.ts
import { Router, Request, Response } from "express";
import { TodoService } from "../services/todoService";
import type { CreateTodoDto, UpdateTodoDto } from "@shared/types";

const router = Router();
const todoService = new TodoService();

router.get("/", async (req: Request, res: Response) => {
  const todos = await todoService.findAll();
  res.json({ data: todos });
});

router.get("/:id", async (req: Request<{ id: string }>, res: Response) => {
  const id = Number(req.params.id);
  const todo = await todoService.findById(id);

  if (!todo) {
    res.status(404).json({ error: "Todo not found" });
    return;
  }

  res.json({ data: todo });
});

router.post("/", async (req: Request<{}, {}, CreateTodoDto>, res: Response) => {
  const todo = await todoService.create(req.body);
  res.status(201).json({ data: todo, message: "Created" });
});

router.put("/:id", async (req: Request<{ id: string }, {}, UpdateTodoDto>, res: Response) => {
  const id = Number(req.params.id);
  const todo = await todoService.update(id, req.body);
  res.json({ data: todo, message: "Updated" });
});

router.delete("/:id", async (req: Request<{ id: string }>, res: Response) => {
  const id = Number(req.params.id);
  await todoService.delete(id);
  res.status(204).send();
});

export default router;

前端实现

// client/src/api/todos.ts
import type { Todo, CreateTodoDto, UpdateTodoDto, ApiResponse } from "@shared/types";

const BASE_URL = "/api/todos";

async function request<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      ...options?.headers
    }
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || `HTTP ${response.status}`);
  }

  return response.json();
}

export const todosApi = {
  getAll(): Promise<ApiResponse<Todo[]>> {
    return request(BASE_URL);
  },

  create(data: CreateTodoDto): Promise<ApiResponse<Todo>> {
    return request(BASE_URL, {
      method: "POST",
      body: JSON.stringify(data)
    });
  },

  update(id: number, data: UpdateTodoDto): Promise<ApiResponse<Todo>> {
    return request(`${BASE_URL}/${id}`, {
      method: "PUT",
      body: JSON.stringify(data)
    });
  },

  delete(id: number): Promise<void> {
    return request(`${BASE_URL}/${id}`, {
      method: "DELETE"
    });
  }
};
// client/src/hooks/useTodos.ts
import { useState, useEffect, useCallback } from "react";
import { todosApi } from "../api/todos";
import type { Todo, CreateTodoDto } from "@shared/types";

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchTodos = useCallback(async () => {
    try {
      setLoading(true);
      const response = await todosApi.getAll();
      setTodos(response.data);
      setError(null);
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchTodos();
  }, [fetchTodos]);

  const addTodo = async (data: CreateTodoDto) => {
    const response = await todosApi.create(data);
    setTodos(prev => [response.data, ...prev]);
  };

  const toggleTodo = async (id: number, completed: boolean) => {
    const response = await todosApi.update(id, { completed: !completed });
    setTodos(prev => prev.map(t => (t.id === id ? response.data : t)));
  };

  const deleteTodo = async (id: number) => {
    await todosApi.delete(id);
    setTodos(prev => prev.filter(t => t.id !== id));
  };

  return { todos, loading, error, addTodo, toggleTodo, deleteTodo };
}
// client/src/App.tsx
import { useTodos } from "./hooks/useTodos";
import { TodoList } from "./components/TodoList";
import { AddTodo } from "./components/AddTodo";

function App() {
  const { todos, loading, error, addTodo, toggleTodo, deleteTodo } = useTodos();

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div className="app">
      <h1>待办事项</h1>
      <AddTodo onAdd={addTodo} />
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
    </div>
  );
}

export default App;

项目二:类型安全的工具库

项目结构

ts-utils/
├── src/
│   ├── array.ts
│   ├── object.ts
│   ├── string.ts
│   ├── validation.ts
│   └── index.ts
├── tests/
│   ├── array.test.ts
│   ├── object.test.ts
│   └── validation.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts

类型安全的数组工具

// src/array.ts

// 类型守卫过滤
export function isDefined<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// 去重(支持自定义键)
export function uniqueBy<T, K extends keyof T>(
  arr: T[],
  key: K
): T[] {
  const seen = new Set<T[K]>();
  return arr.filter(item => {
    const val = item[key];
    if (seen.has(val)) return false;
    seen.add(val);
    return true;
  });
}

// 分组
export function groupBy<T, K extends string | number>(
  arr: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  return arr.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
    return groups;
  }, {} as Record<K, T[]>);
}

// 分页
export function paginate<T>(
  arr: T[],
  page: number,
  pageSize: number
): { data: T[]; total: number; page: number; pageSize: number; totalPages: number } {
  const total = arr.length;
  const totalPages = Math.ceil(total / pageSize);
  const start = (page - 1) * pageSize;
  const data = arr.slice(start, start + pageSize);

  return { data, total, page, pageSize, totalPages };
}

// 按条件排序
export function sortBy<T>(
  arr: T[],
  ...criteria: Array<{ key: keyof T; direction?: "asc" | "desc" }>
): T[] {
  return [...arr].sort((a, b) => {
    for (const { key, direction = "asc" } of criteria) {
      const aVal = a[key];
      const bVal = b[key];
      const modifier = direction === "asc" ? 1 : -1;

      if (aVal < bVal) return -1 * modifier;
      if (aVal > bVal) return 1 * modifier;
    }
    return 0;
  });
}

类型安全的验证器

// src/validation.ts

type Validator<T> = {
  validate: (value: unknown) => value is T;
  message: string;
};

// 基本验证器
export const validators = {
  string: (): Validator<string> => ({
    validate: (value): value is string => typeof value === "string",
    message: "Must be a string"
  }),

  number: (): Validator<number> => ({
    validate: (value): value is number => typeof value === "number" && !isNaN(value),
    message: "Must be a number"
  }),

  boolean: (): Validator<boolean> => ({
    validate: (value): value is boolean => typeof value === "boolean",
    message: "Must be a boolean"
  }),

  array: <T>(itemValidator: Validator<T>): Validator<T[]> => ({
    validate: (value): value is T[] =>
      Array.isArray(value) && value.every(item => itemValidator.validate(item)),
    message: `Must be an array of ${itemValidator.message}`
  }),

  optional: <T>(validator: Validator<T>): Validator<T | undefined> => ({
    validate: (value): value is T | undefined =>
      value === undefined || validator.validate(value),
    message: `Optional ${validator.message}`
  }),

  minLength: (min: number): Validator<string> => ({
    validate: (value): value is string =>
      typeof value === "string" && value.length >= min,
    message: `Must be at least ${min} characters`
  }),

  maxLength: (max: number): Validator<string> => ({
    validate: (value): value is string =>
      typeof value === "string" && value.length <= max,
    message: `Must be at most ${max} characters`
  }),

  pattern: (regex: RegExp): Validator<string> => ({
    validate: (value): value is string =>
      typeof value === "string" && regex.test(value),
    message: `Must match pattern ${regex}`
  })
};

// Schema 验证器
type Schema<T> = {
  [K in keyof T]: Validator<T[K]>;
};

export function createValidator<T extends Record<string, any>>(
  schema: Schema<T>
): (data: unknown) => { success: true; data: T } | { success: false; errors: Record<keyof T, string> } {
  return (data: unknown) => {
    if (typeof data !== "object" || data === null) {
      return { success: false, errors: {} as Record<keyof T, string> };
    }

    const errors = {} as Record<keyof T, string>;
    const result = {} as T;
    let hasErrors = false;

    for (const [key, validator] of Object.entries(schema)) {
      const value = (data as Record<string, unknown>)[key];
      if (validator.validate(value)) {
        (result as any)[key] = value;
      } else {
        errors[key as keyof T] = validator.message;
        hasErrors = true;
      }
    }

    return hasErrors ? { success: false, errors } : { success: true, data: result };
  };
}

// 使用
const userValidator = createValidator({
  name: validators.string(),
  email: validators.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
  age: validators.number()
});

const result = userValidator({ name: "Alice", email: "alice@test.com", age: 25 });
if (result.success) {
  console.log(result.data.name); // 类型安全
}

项目三:CLI 工具开发

项目结构

my-cli/
├── src/
│   ├── index.ts
│   ├── commands/
│   │   ├── init.ts
│   │   ├── build.ts
│   │   └── deploy.ts
│   ├── utils/
│   │   ├── config.ts
│   │   └── logger.ts
│   └── types.ts
├── package.json
└── tsconfig.json

CLI 框架

// src/types.ts
export interface CommandContext {
  cwd: string;
  config: AppConfig;
  logger: Logger;
}

export interface Command {
  name: string;
  description: string;
  options: CommandOption[];
  execute: (args: string[], options: Record<string, any>, ctx: CommandContext) => Promise<void>;
}

export interface CommandOption {
  name: string;
  alias?: string;
  description: string;
  required?: boolean;
  default?: any;
}

export interface AppConfig {
  name: string;
  version: string;
  [key: string]: any;
}

export interface Logger {
  info(message: string): void;
  warn(message: string): void;
  error(message: string): void;
  success(message: string): void;
}
// src/utils/logger.ts
import chalk from "chalk";
import type { Logger } from "../types";

export const logger: Logger = {
  info: (msg) => console.log(chalk.blue("ℹ"), msg),
  warn: (msg) => console.log(chalk.yellow("⚠"), msg),
  error: (msg) => console.log(chalk.red("✖"), msg),
  success: (msg) => console.log(chalk.green("✔"), msg)
};
// src/utils/config.ts
import fs from "fs/promises";
import path from "path";
import type { AppConfig } from "../types";

const CONFIG_FILE = "my-cli.config.json";

export async function loadConfig(cwd: string): Promise<AppConfig> {
  const configPath = path.join(cwd, CONFIG_FILE);

  try {
    const content = await fs.readFile(configPath, "utf-8");
    return JSON.parse(content);
  } catch {
    return {
      name: path.basename(cwd),
      version: "1.0.0"
    };
  }
}

export async function saveConfig(cwd: string, config: AppConfig): Promise<void> {
  const configPath = path.join(cwd, CONFIG_FILE);
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
}
// src/commands/init.ts
import inquirer from "inquirer";
import { saveConfig } from "../utils/config";
import type { Command, AppConfig } from "../types";

export const initCommand: Command = {
  name: "init",
  description: "Initialize a new project",
  options: [
    { name: "name", alias: "n", description: "Project name" }
  ],

  async execute(args, options, ctx) {
    const answers = await inquirer.prompt([
      {
        type: "input",
        name: "name",
        message: "Project name:",
        default: options.name || ctx.config.name
      },
      {
        type: "list",
        name: "template",
        message: "Template:",
        choices: ["basic", "react", "node"]
      },
      {
        type: "confirm",
        name: "typescript",
        message: "Use TypeScript?",
        default: true
      }
    ]);

    const config: AppConfig = {
      name: answers.name,
      version: "1.0.0",
      template: answers.template,
      typescript: answers.typescript
    };

    await saveConfig(ctx.cwd, config);
    ctx.logger.success(`Project "${config.name}" initialized!`);
  }
};
// src/index.ts
import { Command } from "commander";
import { loadConfig } from "./utils/config";
import { logger } from "./utils/logger";
import { initCommand } from "./commands/init";
import { buildCommand } from "./commands/build";
import { deployCommand } from "./commands/deploy";
import type { CommandContext } from "./types";

const program = new Command();

program
  .name("my-cli")
  .description("A TypeScript CLI tool")
  .version("1.0.0");

async function createContext(): Promise<CommandContext> {
  const cwd = process.cwd();
  const config = await loadConfig(cwd);
  return { cwd, config, logger };
}

// 注册命令
const commands = [initCommand, buildCommand, deployCommand];

for (const cmd of commands) {
  const subCommand = program
    .command(cmd.name)
    .description(cmd.description);

  for (const opt of cmd.options) {
    subCommand.option(
      opt.alias ? `-${opt.alias}, --${opt.name}` : `--${opt.name}`,
      opt.description,
      opt.default
    );
  }

  subCommand.action(async (...args) => {
    const options = args[args.length - 1].opts();
    const ctx = await createContext();
    await cmd.execute(args.slice(0, -1), options, ctx);
  });
}

program.parse();

package.json

{
  "name": "my-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-cli": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "chalk": "^5.3.0",
    "commander": "^12.0.0",
    "inquirer": "^9.2.0"
  },
  "devDependencies": {
    "@types/inquirer": "^9.0.0",
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "tsx": "^4.0.0"
  }
}

总结

TypeScript 在不同场景下的应用

场景推荐方案
React 前端Vite + TypeScript + React
Node.js 后端Express/Fastify + TypeScript
npm 库tsc + esbuild
CLI 工具Commander + TypeScript
全栈应用Monorepo + 共享类型
测试Jest/Vitest + TypeScript

学习路线

入门 → 基础类型 → 函数 → 接口 → 类
  ↓
核心 → 泛型 → 联合/交叉 → 类型收窄 → 工具类型
  ↓
高级 → 条件类型 → 映射类型 → 模板字面量 → 类型体操
  ↓
实战 → React/Node.js → 测试 → 构建 → 部署

扩展阅读