08 - 实战技巧与最佳实践

3 阅读6分钟

本章汇总 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 最佳实践清单

类型相关

  1. 始终开启 strict: true —— 这是 TypeScript 最大的价值
  2. 避免使用 any —— 用 unknown 替代,或想办法写出正确的类型
  3. 优先使用类型推断 —— 不需要处处写类型注解
  4. 导出的函数/接口显式写类型 —— 公开 API 的类型应该明确
  5. 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"

代码组织

  1. 类型和实现分离 —— 复杂项目中把类型放在 types.tstypes/ 目录
  2. 用桶文件管理导出 —— 每个目录一个 index.ts
  3. 避免循环依赖 —— 类型文件可以单独成模块避免循环

安全性

  1. 用可辨识联合处理不同状态
// ✅ 好
type State =
  | { status: "loading" }
  | { status: "success"; data: User[] }
  | { status: "error"; error: string };

// ❌ 不好
type State = {
  loading: boolean;
  data?: User[];
  error?: string;
};
  1. 函数参数用对象而非多个参数
// ❌ 参数多了容易搞混
function createUser(name: string, age: number, email: string, role: string) {}

// ✅ 用对象参数 + 解构
function createUser(params: {
  name: string;
  age: number;
  email: string;
  role: string;
}) {}
  1. 用 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); // ❌ 编译错误!

性能

  1. skipLibCheck: true —— 加速编译
  2. 使用 project references —— 大型 monorepo 分割编译
  3. 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 实现一个简单的待办事项管理器

// 完整实现参考

// 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 }