第 7 章:反模式 —— 你可能正在犯的错误
在前面的章节中,我们探讨了 interface 和 type 的正确使用方式。然而,在实际项目中,开发者经常会陷入一些反模式。在这一章中,我们将识别并分析这些常见的错误,帮助你写出更好的类型定义。
盲目 Omit<yargs.CommandModule, 'command'>
一个常见的反模式是盲目使用 Omit 来去除某些属性,而不考虑实际需求:
typescript
// 反模式:盲目使用 Omit
import yargs from 'yargs';
type CustomCommand = Omit<yargs.CommandModule, 'command'> & {
command: string | string[];
};
// 更好的做法:明确你需要的属性
type CustomCommand = {
command: string | string[];
describe?: string | false;
builder?: (args: yargs.Argv) => yargs.Argv;
handler: (args: yargs.Arguments) => void | Promise<void>;
};盲目的类型操作会导致类型定义过于宽泛或丢失重要的类型信息。更好的做法是明确你需要哪些属性,并重新定义类型。
为了"可扩展"用 interface,结果没人扩展
另一种常见的反模式是为了潜在的扩展性而使用 interface,但实际上从未被扩展:
typescript
// 反模式:过度设计
interface UserProfile {
name: string;
email: string;
}
// 在整个项目中只有这一个实现,从未被扩展过
// 更好的做法:使用 type
type UserProfile = {
name: string;
email: string;
};如果你的类型只有一种用途且从未被扩展,那么使用 type 更加合适。过度设计只会增加不必要的复杂性。
在 DTO 中用 interface 导致意外合并
在定义 DTO(Data Transfer Object)时使用 interface 可能会导致意外的声明合并:
typescript
// 反模式:在不同文件中意外定义了同名 interface
// file1.ts
interface UserResponse {
id: number;
name: string;
}
// file2.ts
interface UserResponse {
email: string;
}
// 结果合并后的 UserResponse 包含了所有属性
// 如果这不是你想要的,就会导致问题
// 更好的做法:使用 type
// file1.ts
type UserResponse = {
id: number;
name: string;
};
// file2.ts
type UserResponse = {
email: string;
}; // 这里会报错,防止意外合并在定义数据传输对象时,通常不希望发生声明合并,因为这可能会引入非预期的属性。
混淆交叉类型和继承
有时候开发者会混淆交叉类型(Intersection Types)和接口继承:
typescript
// 可能引起混淆的写法
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
// 方式1:交叉类型
type WorkingPerson = Person & Employee;
// 方式2:接口继承
interface WorkingPerson extends Person, Employee {}
// 这两种方式看似相同,但在某些情况下会有差异虽然在大多数情况下这两种方式效果相似,但在处理同名属性、函数重载等复杂情况时可能会有细微差别。
过度嵌套的类型定义
另一个常见问题是过度嵌套的类型定义,使得类型难以理解和维护:
typescript
// 反模式:过度嵌套
type ComplexType = {
user: {
profile: {
personal: {
name: string;
address: {
street: string;
city: string;
country: string;
};
};
professional: {
company: string;
position: string;
skills: string[];
};
};
};
};
// 更好的做法:分解为小的、可重用的类型
type Address = {
street: string;
city: string;
country: string;
};
type PersonalInfo = {
name: string;
address: Address;
};
type ProfessionalInfo = {
company: string;
position: string;
skills: string[];
};
type UserProfile = {
personal: PersonalInfo;
professional: ProfessionalInfo;
};
type UserType = {
user: {
profile: UserProfile;
};
};分解类型不仅可以提高可读性,还可以促进类型重用。
忽视联合类型的完备性检查
在使用联合类型时,容易忽视对所有情况的处理:
typescript
// 反模式:没有处理所有情况
type Status = 'loading' | 'success' | 'error';
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: any;
}
interface ErrorState {
status: 'error';
error: Error;
}
type AppState = LoadingState | SuccessState | ErrorState;
// 忘记处理 error 状态
function renderState(state: AppState) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return state.data;
// 忘记处理 'error' 情况,TypeScript 不会警告!
}
}
// 更好的做法:使用类型守卫确保完备性
function renderStateComplete(state: AppState) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return state.data;
case 'error':
return `Error: ${state.error.message}`;
default:
// 这里使用 never 类型确保所有情况都被处理
const exhaustiveCheck: never = state;
throw new Error(`Unhandled state: ${exhaustiveCheck}`);
}
}使用 never 类型可以帮助 TypeScript 检查是否处理了所有可能的情况。
小结
在本章中,我们探讨了几种常见的反模式:
- 盲目使用
Omit等类型操作符 - 为了潜在扩展性而过度使用
interface - 在 DTO 中使用
interface导致意外合并 - 混淆交叉类型和接口继承
- 过度嵌套的类型定义
- 忽视联合类型的完备性检查
避免这些反模式可以让你的类型定义更加清晰、健壮和易于维护。
下一章,我们将提供一个可视化的决策树,帮助你在面对具体选择时快速做出判断。
思考题: 你在项目中是否遇到过上述提到的反模式?你是如何发现并解决这些问题的?