24 - 最佳实践
最佳实践
类型设计原则
1. 优先使用 interface 定义对象
// ✅ 推荐:使用 interface
interface User {
id: number;
name: string;
email: string;
}
// ⚠️ 也可以:使用 type
type User = {
id: number;
name: string;
email: string;
};
// 选择建议:
// - 对象结构 → interface(支持声明合并和 extends)
// - 联合类型、交叉类型、工具类型 → type
2. 避免使用 any
// ❌ 不好
function process(data: any): any {
return data;
}
// ✅ 好:使用 unknown
function process(data: unknown): unknown {
if (typeof data === "string") {
return data.toUpperCase();
}
return data;
}
// ✅ 好:使用泛型
function process<T>(data: T): T {
return data;
}
3. 使用可辨识联合代替类型断言
// ❌ 不好:使用类型断言
function handleResponse(response: any) {
if (response.success) {
const data = response.data as User;
console.log(data.name);
}
}
// ✅ 好:使用可辨识联合
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
if (response.success) {
console.log(response.data); // 自动收窄
}
}
4. 使用 as const
// ❌ 不好:宽泛类型
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact"
};
// type: { home: string; about: string; contact: string }
// ✅ 好:字面量类型
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact"
} as const;
// type: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
// 配合联合类型
type Route = typeof ROUTES[keyof typeof ROUTES]; // "/" | "/about" | "/contact"
类型体操(Type Gymnastics)
常用类型变换
// DeepPartial - 递归可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// DeepReadonly - 递归只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// NonNullable 递归版本
type DeepNonNullable<T> = T extends null | undefined
? never
: T extends object
? { [K in keyof T]: DeepNonNullable<T[K]> }
: T;
// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
// 构建元组
type BuildTuple<N extends number, T extends any[] = []> =
T["length"] extends N ? T : BuildTuple<N, [...T, any]>;
条件类型技巧
// 类型过滤
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
type StringProps = FilterByType<User, string>; // { name: string; email: string }
type NumberProps = FilterByType<User, number>; // { id: number }
// 提取可选属性
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
// 提取必选属性
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
性能优化
1. 使用 type 导入
// ✅ 使用 import type(编译后被移除)
import type { User, UserRole } from "./types";
import type * as Types from "./types";
// ✅ 内联 type 导入
import { createUser, type User, type UserRole } from "./services/user";
2. 避免过度使用泛型
// ❌ 不好:过度泛型
function identity<T>(value: T): T {
return value;
}
// 简单场景直接使用具体类型
// ✅ 好:根据需要使用泛型
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
3. 减少类型推断的复杂度
// ❌ 不好:复杂的嵌套类型推断
const result = deeply.nested.object.with.many.properties;
// ✅ 好:提取类型
type DeepType = typeof deeply.nested.object.with.many.properties;
const result: DeepType = deeply.nested.object.with.many.properties;
4. 使用 interface 而非交叉类型
// ❌ 不好:交叉类型(性能较差)
type User = { id: number } & { name: string } & { email: string };
// ✅ 好:单个 interface(性能更好)
interface User {
id: number;
name: string;
email: string;
}
代码规范
命名约定
// 类型和接口:PascalCase
interface UserProfile { }
type ApiResponse = { };
// 泛型参数:单字母或 T 前缀
function identity<T>(value: T): T { }
function process<TItem, TResult>(items: TItem[]): TResult { }
// 类型别名:描述性名称
type UserId = number;
type UserMap = Record<string, User>;
// 枚举:PascalCase
enum UserRole { Admin, User, Guest }
// 常量:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = "https://api.example.com";
文件组织
src/
├── types/ # 类型定义
│ ├── index.ts # 导出
│ ├── user.ts # 用户相关类型
│ └── api.ts # API 相关类型
├── utils/
│ └── helpers.ts
└── index.ts
导出规范
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
}
export type CreateUserDto = Omit<User, "id">;
export type UpdateUserDto = Partial<CreateUserDto>;
// types/index.ts(barrel 文件)
export type { User, CreateUserDto, UpdateUserDto } from "./user";
export type { ApiResponse, PaginatedResponse } from "./api";
常见陷阱
1. 对象字面量的多余属性检查
interface User {
name: string;
age: number;
}
// ❌ 直接传递对象字面量会触发多余属性检查
const user: User = {
name: "Alice",
age: 25,
email: "[email protected]" // ❌ 错误
};
// ✅ 通过变量传递不会触发
const userData = {
name: "Alice",
age: 25,
email: "[email protected]"
};
const user: User = userData; // ✅ 兼容
2. 数组的 filter 类型收窄
const items: (string | number)[] = [1, "two", 3, "four"];
// ❌ filter 不会自动收窄类型
const strings = items.filter(item => typeof item === "string");
// 类型仍然是 (string | number)[]
// ✅ 使用类型守卫
function isString(value: unknown): value is string {
return typeof value === "string";
}
const strings = items.filter(isString); // string[]
3. 闭包中的类型收窄
function example(value: string | null) {
if (value !== null) {
// value: string ✅
setTimeout(() => {
// ❌ value 可能为 null(闭包问题)
// console.log(value.toUpperCase());
// ✅ 解决:捕获到局部变量
const safeValue = value;
console.log(safeValue.toUpperCase());
}, 100);
}
}
4. 类型断言的滥用
// ❌ 不好:随意使用 as
const data = JSON.parse(str) as User;
// ✅ 好:验证后使用
const parsed = JSON.parse(str);
if (
typeof parsed === "object" &&
parsed !== null &&
"name" in parsed &&
"email" in parsed
) {
const user = parsed as User;
}
5. 枚举的陷阱
// ❌ 数字枚举不安全
enum Status {
Active, // 0
Inactive, // 1
}
const s: Status = 42; // ✅ 编译通过(任何数字都兼容)
// ✅ 字符串枚举更安全
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
const s: Status = "ACTIVE"; // ✅
// const s: Status = "INVALID"; // ❌
ESLint 配置
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "warn"
}
}
业务场景:类型安全的配置管理
// 配置类型
interface AppConfig {
server: {
port: number;
host: string;
ssl: boolean;
};
database: {
url: string;
pool: number;
};
auth: {
secret: string;
expiresIn: string;
};
}
// 验证配置
function validateConfig(config: unknown): AppConfig {
if (typeof config !== "object" || config === null) {
throw new Error("Config must be an object");
}
const c = config as Record<string, unknown>;
// 验证每个字段
if (typeof c.server !== "object") throw new Error("Missing server config");
// ... 更多验证
return config as AppConfig;
}
// 使用
const config = validateConfig(JSON.parse(
await fs.readFile("config.json", "utf-8")
));
注意事项
- 类型是文档——好的类型定义是最好的文档
- 不要过度类型化——简单场景不需要复杂类型
- 优先使用 unknown 而非 any
- as const 可以让值变为类型
- 定期运行
tsc --noEmit 检查类型错误
扩展阅读