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 → 测试 → 构建 → 部署
扩展阅读