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) 代码审查检查清单
- [ ] 没有
any类型(除非有注释说明原因) - [ ] 函数参数和返回值有类型注解
- [ ] 可能为 null/undefined 的值做了检查
- [ ] 没有使用
@ts-ignore(用@ts-expect-error替代) - [ ] 公共 API 有 JSDoc 注释
- [ ] 类型导入用了
import type
❓ 常见问题
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) 用泛型默认值减少必须指定的参数。类型参数过多通常意味着抽象层次不合适。
📖 小节
- 命名规范:接口/类型/类用 PascalCase,泛型用单字母或 PascalCase
- 类型设计:精确优于宽泛、推算优于手写、组合优于继承
- any 替代方案:unknown(安全兜底)、泛型(保留类型)、联合类型(明确枚举)
- DRY 类型:用 keyof/typeof/工具类型从源推算,不重复定义
- 类型收窄:尽早收窄、复用类型守卫
- 团队规范:统一 tsconfig、ESLint 规则、代码审查检查清单
📝 作业
- 基础题(难度⭐):找一个自己写过或看到的 any 类型使用场景,用 unknown 或泛型替代它。确保替代后功能不变且类型更安全。
- 进阶题(难度⭐⭐):用 DRY 原则重构一组类型定义——给定
ApiProduct接口,用工具类型派生出CreateProduct、UpdateProduct、ProductSummary、ProductResponse四个类型,不手写任何重复属性。 - 挑战题(难度⭐⭐⭐):为团队编写 TypeScript 代码规范文档——包含命名规范、禁止 any 的替代方案、类型导入规则、tsconfig 推荐配置、ESLint 推荐规则。给出每条规范的理由和正反例。