TypeScript: TypeScript 最佳实践

最后更新:2026-08-26

掌握语法只是起点——写好 TypeScript 需要遵循一套最佳实践。本课总结实际项目中最有价值的经验和原则。

1. 命名规范

(1) 类型命名

类别 规范 示例
接口 PascalCase UserService, ApiResponse
类型别名 PascalCase Status, EventHandler
泛型参数 单字母或 PascalCase T, K, TItem
枚举 PascalCase(值可大写) HttpStatus, COLOR_RED
枚举成员 PascalCase HttpStatus.Ok

(2) 文件命名

类型 规范 示例
普通模块 camelCase userService.ts
类文件 PascalCase UserController.ts
声明文件 与模块同名 lodash.d.ts
测试文件 模块名.test userService.test.ts

(3) 导出规范

TYPESCRIPT
// ✅ 推荐——命名导出为主
export function addUser(user: User): void { }
export class UserController { }
export type Status = "active" | "inactive";

// ⚠️ 谨慎使用默认导出——重命名时容易出错
export default class UserController { }

// ✅ 库/框架推荐——同时提供两种
export class UserController { }
export default UserController;

2. 类型设计原则

(1) 原则一:精确优于宽泛

TYPESCRIPT
// ❌ 宽泛——失去了类型信息
function process(value: any): any { }

// ❌ 稍好但仍宽泛
function process(value: string | number): string | number { }

// ✅ 精确——泛型保留类型信息
function process<T extends string | number>(value: T): T { }

(2) 原则二:推算优于手写

TYPESCRIPT
// ❌ 手动维护两处——容易不同步
interface User { id: number; name: string; email: string; }
type UserKeys = "id" | "name" | "email";

// ✅ 从源类型推算——自动同步
type UserKeys2 = keyof User;  // "id" | "name" | "email"
type UserValues = User[keyof User];  // number | string

(3) 原则三:组合优于继承

TYPESCRIPT
// ❌ 深层继承——脆弱的基类问题
class BaseEntity { id: number; }
class TimestampedEntity extends BaseEntity { createdAt: Date; }
class FullEntity extends TimestampedEntity { createdBy: string; }

// ✅ 类型组合——灵活且解耦
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithAudit = { createdBy: string; updatedBy: string };

type FullEntity2 = WithId & WithTimestamps & WithAudit;
type SimpleEntity = WithId;  // 按需组合

▶ 示例:any 重构为类型安全

TYPESCRIPT
// ❌ 重构前——到处 any,类型安全为零
function processRequest(req: any): any {
  let user = req.body.user;       // any
  let result = validate(user);    // any
  return { status: 200, data: result };
}

// ✅ 重构后——每一步都有类型保护
interface User { id: number; name: string; email: string; }
interface Request2 { body: { user: User } }
interface ValidationResult { valid: boolean; errors?: string[] }
interface Response2<T> { status: number; data: T }

function processRequest2(req: Request2): Response2<ValidationResult> {
  let user: User = req.body.user;             // ✅ User 类型
  let result: ValidationResult = validate2(user);  // ✅ ValidationResult
  return { status: 200, data: result };
}

function validate2(user: User): ValidationResult {
  if (!user.email.includes("@")) {
    return { valid: false, errors: ["邮箱格式错误"] };
  }
  return { valid: true };
}
▶ 试一试

3. any 的替代方案

(1) unknown 替代 any

TYPESCRIPT
// ❌ any——关闭类型检查
function process(value: any) {
  return value.toUpperCase();  // 不检查,运行时可能崩溃
}

// ✅ unknown——必须先收窄才能使用
function process2(value: unknown) {
  if (typeof value === "string") {
    return value.toUpperCase();  // ✅ 收窄后安全使用
  }
  throw new Error("期望 string 类型");
}

(2) 泛型替代 any

TYPESCRIPT
// ❌ any——丢失类型信息
function first(arr: any[]): any {
  return arr[0];
}

// ✅ 泛型——保留类型信息
function first2<T>(arr: T[]): T {
  return arr[0];
}

(3) 联合类型替代 any

TYPESCRIPT
// ❌ any
let value: any;

// ✅ 联合类型——明确列出可能的类型
let value2: string | number | boolean;

(4) 索引签名替代 any 对象

TYPESCRIPT
// ❌ any 对象
let config: any = { host: "localhost" };

// ✅ 索引签名
let config2: Record<string, string | number> = { host: "localhost" };

4. DRY 类型(不重复自己)

(1) 用 keyof、typeof、工具类型避免重复

TYPESCRIPT
const THEMES = {
  light: { bg: "#fff", text: "#333" },
  dark: { bg: "#1a1a1a", text: "#e0e0e0" }
} as const;

// 从值推算类型——不重复定义
type ThemeName = keyof typeof THEMES;  // "light" | "dark"
type ThemeColors = typeof THEMES["light"];  // { readonly bg: "..."; readonly text: "..." }

function getTheme(name: ThemeName): ThemeColors {
  return THEMES[name];
}

(2) 用映射类型批量变换

TYPESCRIPT
interface ApiUser {
  id: number;
  name: string;
  email: string;
  role: string;
}

// 不需要手写——用工具类型派生
type CreateUserDTO = Omit<ApiUser, "id">;
type UpdateUserDTO = Partial<Omit<ApiUser, "id">>;
type UserSummary = Pick<ApiUser, "id" | "name">;
type UserResponse = Readonly<ApiUser>;

5. 类型收窄策略

(1) 尽早收窄

TYPESCRIPT
// ❌ 延迟收窄——每处使用都要检查
function process(value: string | number) {
  console.log(value.toString());      // 只能用共有方法
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
  // 后续还要再检查...
}

// ✅ 尽早收窄——分支内直接使用
function process2(value: string | number) {
  if (typeof value === "string") {
    // 整个分支都是 string
    console.log(value.toUpperCase());
    console.log(value.trim());
    return;
  }
  // 这里一定是 number
  console.log(value.toFixed(2));
}

(2) 自定义类型守卫复用收窄逻辑

TYPESCRIPT
// 复杂的收窄逻辑——提取为类型守卫
function isValidUser(obj: any): obj is User {
  return obj
    && typeof obj.id === "number"
    && typeof obj.name === "string"
    && typeof obj.email === "string";
}

// 多处复用
function processUser(data: unknown) {
  if (isValidUser(data)) {
    console.log(data.name);  // ✅ 类型安全
  }
}

6. 团队协作规范

(1) tsconfig 统一配置

JSON
{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

团队成员的 IDE 自动应用相同规则——不需要手动配置。

(2) ESLint TypeScript 规则

JSON
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unnecessary-type-assertion": "error",
    "@typescript-eslint/explicit-function-return-type": "warn"
  }
}

(3) 代码审查检查清单


❓ 常见问题

Q 项目里应该完全禁止 any 吗?
A 不现实。ESLint 设置 no-explicit-any: warn 而不是 error——允许少量 any 但提醒审查。第三方库无类型、复杂类型变换、快速原型等场景 any 是合理的。关键是有注释说明原因,且有计划替换。
Q 类型定义该写在单独文件还是就近文件?
A 公共/共享类型放 types/models/ 目录,就近类型(只在单个文件使用的)直接写在文件内。规则:被3个以上文件引用→提取到公共类型文件,否则就近定义。
Q TS 项目要不要用 ESLint?
A 要。tsc 做类型检查,ESLint 做代码质量检查——两者互补不替代。@typescript-eslint 插件提供了 TS 特有的规则(no-explicit-any、consistent-type-imports 等),是 TS 项目的标配。
Q 泛型类型参数太多怎么办?
A 超过3个类型参数时考虑:(1) 用对象参数代替多个泛型 (2) 提取部分类型为独立接口 (3) 用泛型默认值减少必须指定的参数。类型参数过多通常意味着抽象层次不合适。

📖 小节

📝 作业

  1. 基础题(难度⭐):找一个自己写过或看到的 any 类型使用场景,用 unknown 或泛型替代它。确保替代后功能不变且类型更安全。
  2. 进阶题(难度⭐⭐):用 DRY 原则重构一组类型定义——给定 ApiProduct 接口,用工具类型派生出 CreateProductUpdateProductProductSummaryProductResponse 四个类型,不手写任何重复属性。
  3. 挑战题(难度⭐⭐⭐):为团队编写 TypeScript 代码规范文档——包含命名规范、禁止 any 的替代方案、类型导入规则、tsconfig 推荐配置、ESLint 推荐规则。给出每条规范的理由和正反例。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏