本章汇总 TypeScript 在实际项目中的配置、常见模式和最佳实践。
8.1 tsconfig.json 详解
tsconfig.json 是 TypeScript 项目的配置文件,以下是最常用的选项:
{
"compilerOptions": {
// ========== 基础选项 ==========
"target": "ES2020", // 编译目标 JS 版本
"module": "ESNext", // 模块系统
"lib": ["ES2020", "DOM"], // 可用的类型库
"moduleResolution": "bundler", // 模块解析策略
// ========== 严格模式(强烈推荐全开)==========
"strict": true, // 开启所有严格检查,等于下面所有:
// "noImplicitAny": true, // 不允许隐式 any
// "strictNullChecks": true, // 严格空值检查
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "noImplicitThis": true,
// "alwaysStrict": true,
// ========== 额外检查 ==========
"noUnusedLocals": true, // 不允许未使用的变量
"noUnusedParameters": true, // 不允许未使用的参数
"noImplicitReturns": true, // 所有分支都必须有返回值
"noFallthroughCasesInSwitch": true, // switch 必须有 break
// ========== 输出选项 ==========
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 源码根目录
"declaration": true, // 生成 .d.ts 声明文件
"sourceMap": true, // 生成 source map
"removeComments": true, // 移除注释
// ========== 互操作 ==========
"esModuleInterop": true, // 兼容 CommonJS 默认导入
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, // 文件名大小写敏感
// ========== 路径 ==========
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
// ========== 跳过检查 ==========
"skipLibCheck": true // 跳过 .d.ts 检查(加速编译)
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
常见项目配置模板
Vue 项目:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
React 项目:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}
Node.js 项目:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
8.2 常见设计模式
Builder 模式
class RequestBuilder {
private config: {
url: string;
method: string;
headers: Record<string, string>;
body?: any;
};
constructor(url: string) {
this.config = { url, method: "GET", headers: {} };
}
setMethod(method: "GET" | "POST" | "PUT" | "DELETE"): this {
this.config.method = method;
return this; // 返回 this 实现链式调用
}
setHeader(key: string, value: string): this {
this.config.headers[key] = value;
return this;
}
setBody(body: any): this {
this.config.body = body;
return this;
}
async send<T>(): Promise<T> {
const res = await fetch(this.config.url, {
method: this.config.method,
headers: this.config.headers,
body: JSON.stringify(this.config.body),
});
return res.json();
}
}
// 使用
const data = await new RequestBuilder("/api/users")
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setBody({ name: "张三", age: 25 })
.send<{ id: number }>();
策略模式
interface PaymentStrategy {
pay(amount: number): Promise<boolean>;
}
class AlipayStrategy implements PaymentStrategy {
async pay(amount: number) {
console.log(`支付宝支付 ${amount} 元`);
return true;
}
}
class WechatPayStrategy implements PaymentStrategy {
async pay(amount: number) {
console.log(`微信支付 ${amount} 元`);
return true;
}
}
class PaymentContext {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
async checkout(amount: number) {
return this.strategy.pay(amount);
}
}
// 使用
const payment = new PaymentContext(new AlipayStrategy());
await payment.checkout(100);
payment.setStrategy(new WechatPayStrategy());
await payment.checkout(200);
观察者模式(类型安全版)
type EventMap = Record<string, any>;
class TypedEventEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<Function>>();
on<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void,
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
// 返回取消订阅函数
return () => {
this.listeners.get(event)?.delete(listener);
};
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach((fn) => fn(data));
}
}
// 使用
interface ShopEvents {
"item:added": { itemId: string; quantity: number };
"item:removed": { itemId: string };
"cart:cleared": undefined;
}
const shop = new TypedEventEmitter<ShopEvents>();
const unsub = shop.on("item:added", (data) => {
console.log(`添加商品 ${data.itemId},数量 ${data.quantity}`);
});
shop.emit("item:added", { itemId: "abc", quantity: 2 });
unsub(); // 取消订阅
8.3 实战:类型安全的 HTTP 客户端
// 定义 API 路由和类型
interface ApiRoutes {
"GET /users": {
response: User[];
query: { page?: number; limit?: number };
};
"GET /users/:id": {
response: User;
params: { id: string };
};
"POST /users": {
response: User;
body: Omit<User, "id">;
};
"PUT /users/:id": {
response: User;
params: { id: string };
body: Partial<Omit<User, "id">>;
};
"DELETE /users/:id": {
response: void;
params: { id: string };
};
}
interface User {
id: string;
name: string;
email: string;
age: number;
}
// 简化的类型安全请求函数
type RouteConfig = {
response: any;
query?: Record<string, any>;
params?: Record<string, any>;
body?: any;
};
async function api<K extends keyof ApiRoutes>(
route: K,
options?: Omit<ApiRoutes[K], "response">,
): Promise<ApiRoutes[K]["response"]> {
// 实际实现中会解析路由、替换参数等
const [method, path] = (route as string).split(" ");
const res = await fetch(path, {
method,
body: (options as any)?.body ? JSON.stringify((options as any).body) : undefined,
});
return res.json();
}
// 使用 —— 全程类型安全
const users = await api("GET /users"); // User[]
const user = await api("POST /users", {
body: { name: "张三", email: "a@b.com", age: 25 },
});
8.4 最佳实践清单
类型相关
- 始终开启
strict: true—— 这是 TypeScript 最大的价值 - 避免使用
any—— 用unknown替代,或想办法写出正确的类型 - 优先使用类型推断 —— 不需要处处写类型注解
- 导出的函数/接口显式写类型 —— 公开 API 的类型应该明确
- 用
as const替代枚举 —— 更简洁,tree-shaking 友好
// 用 as const 替代枚举
const Status = {
Active: "active",
Inactive: "inactive",
Deleted: "deleted",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// "active" | "inactive" | "deleted"
代码组织
- 类型和实现分离 —— 复杂项目中把类型放在
types.ts或types/目录 - 用桶文件管理导出 —— 每个目录一个
index.ts - 避免循环依赖 —— 类型文件可以单独成模块避免循环
安全性
- 用可辨识联合处理不同状态
// ✅ 好
type State =
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: string };
// ❌ 不好
type State = {
loading: boolean;
data?: User[];
error?: string;
};
- 函数参数用对象而非多个参数
// ❌ 参数多了容易搞混
function createUser(name: string, age: number, email: string, role: string) {}
// ✅ 用对象参数 + 解构
function createUser(params: {
name: string;
age: number;
email: string;
role: string;
}) {}
- 用 branded types 区分相似类型
// 避免把 userId 和 orderId 搞混
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = "user_123" as UserId;
const orderId = "order_456" as OrderId;
getUser(userId); // ✅
getUser(orderId); // ❌ 编译错误!
性能
- 用
skipLibCheck: true—— 加速编译 - 使用 project references —— 大型 monorepo 分割编译
- 用
isolatedModules: true—— 与 babel/swc/esbuild 兼容
8.5 常见问题和解决方案
如何处理 this 指向问题?
class Timer {
count = 0;
// ❌ 普通方法:this 可能丢失
increment() {
this.count++;
}
// ✅ 箭头函数属性:this 永远正确
increment2 = () => {
this.count++;
};
}
如何让对象的 key 类型安全?
// 用 Record + 联合类型
type Config = Record<"development" | "staging" | "production", {
apiUrl: string;
debug: boolean;
}>;
const config: Config = {
development: { apiUrl: "http://localhost:3000", debug: true },
staging: { apiUrl: "https://staging.api.com", debug: true },
production: { apiUrl: "https://api.com", debug: false },
};
如何处理第三方库没有类型的情况?
// 方案一:快速 fix —— 在 declarations.d.ts 中
declare module "untyped-lib";
// 所有导入都是 any
// 方案二:写详细的声明
declare module "untyped-lib" {
export function doThing(input: string): number;
}
// 方案三:安装 @types 包
// npm install --save-dev @types/untyped-lib
如何处理 JSON.parse 的返回值?
// ❌ JSON.parse 返回 any
const data = JSON.parse(jsonString);
// ✅ 方案一:类型断言 + 验证
const data = JSON.parse(jsonString) as User;
// ✅ 方案二:用 zod 等库做运行时验证(推荐)
import { z } from "zod";
const UserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const data = UserSchema.parse(JSON.parse(jsonString)); // 类型安全 + 运行时安全
8.6 推荐工具和资源
开发工具
- VS Code + TypeScript 插件(内置)
- ESLint +
@typescript-eslint—— 代码规范 - Prettier —— 代码格式化
运行时验证库
- Zod —— 最流行的 TS-first 验证库
- Valibot —— 轻量替代品
- io-ts —— 函数式风格
学习资源
- TypeScript 官方文档
- TypeScript Playground —— 在线试验
- Type Challenges —— 类型体操练习
- Total TypeScript —— Matt Pocock 的教程
📝 综合练习
用 TypeScript 实现一个简单的待办事项管理器:
// 完整实现参考
// 1. 定义类型
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
tags: string[];
}
type CreateTodoInput = Pick<Todo, "title"> & { tags?: string[] };
type UpdateTodoInput = Partial<Pick<Todo, "title" | "completed" | "tags">>;
type TodoFilter = "all" | "active" | "completed";
// 2. 实现管理器
class TodoManager {
private todos: Map<string, Todo> = new Map();
private nextId = 1;
private generateId(): string {
return `todo_${this.nextId++}`;
}
add(input: CreateTodoInput): Todo {
const todo: Todo = {
id: this.generateId(),
title: input.title,
completed: false,
createdAt: new Date(),
tags: input.tags ?? [],
};
this.todos.set(todo.id, todo);
return todo;
}
update(id: string, input: UpdateTodoInput): Todo {
const todo = this.todos.get(id);
if (!todo) throw new Error(`Todo ${id} not found`);
const updated = { ...todo, ...input };
this.todos.set(id, updated);
return updated;
}
delete(id: string): boolean {
return this.todos.delete(id);
}
get(id: string): Todo | undefined {
return this.todos.get(id);
}
list(filter: TodoFilter = "all"): Todo[] {
const all = Array.from(this.todos.values());
switch (filter) {
case "active":
return all.filter((t) => !t.completed);
case "completed":
return all.filter((t) => t.completed);
default:
return all;
}
}
findByTag(tag: string): Todo[] {
return Array.from(this.todos.values()).filter((t) =>
t.tags.includes(tag),
);
}
stats(): { total: number; active: number; completed: number } {
const all = Array.from(this.todos.values());
return {
total: all.length,
active: all.filter((t) => !t.completed).length,
completed: all.filter((t) => t.completed).length,
};
}
}
// 3. 使用
const manager = new TodoManager();
const todo1 = manager.add({ title: "学习 TypeScript", tags: ["学习"] });
const todo2 = manager.add({ title: "写项目", tags: ["工作", "编程"] });
manager.update(todo1.id, { completed: true });
console.log(manager.list("active")); // [todo2]
console.log(manager.findByTag("学习")); // [todo1]
console.log(manager.stats()); // { total: 2, active: 1, completed: 1 }