第 8 章:决策树 —— 一张图教你选 type 还是 interface
经过前面几章的学习,我们已经了解了 interface 和 type 的本质区别、适用场景以及常见反模式。但是当我们真正面临选择时,如何快速做出正确的决策呢?
在这一章中,我们将提供一个清晰的决策树,帮助你在面对具体选择时快速做出判断。
决策树详解
┌─────────────┐
│ 你在定义一个类的契约吗? │
└─────────────┘
│
┌───────────────┴───────────────┐
是 否
│ │
┌───────────┴───────────┐ ┌─────────────┴─────────────┐
│ 要被多个类实现吗? │ │ 是数据组合或配置吗? │
└───────────────────────┘ └──────────────────────────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
是 否 是 否
│ │ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼────────┐ ┌────▼────────┐
│ 用 interface │ │ 用 type │ │ 用 type │ │ 用 type │
└─────────────┘ └─────────────┘ └────────────────┘ └─────────────┘让我们详细解释这个决策树的每一步:
第一步:你在定义一个类的契约吗?
这个问题帮助我们区分面向对象编程和函数式编程的场景。
选择"是"的情况:
- 你要定义一个类必须实现的方法和属性
- 你需要使用
implements关键字 - 你在设计面向对象的架构
选择"否"的情况:
- 你只是在给一个复杂类型起别名
- 你在定义数据结构(如 API 响应、配置对象等)
- 你在使用函数式编程范式
第二步(左分支):要被多个类实现吗?
如果你确定在定义类的契约,接下来要考虑的是这个契约是否会被多个类实现。
选择"是"的情况:
- 你定义的是通用接口等
- 你需要多态性,即同一接口的不同实现
- 你在设计插件系统或框架
选择"否"的情况:
- 只有一个类会实现这个接口
- 你只是为了组织代码而创建接口
第二步(右分支):是数据组合或配置吗?
如果你不是在定义类的契约,那么你很可能在处理数据。
选择"是"的情况:
- 定义 React 组件的 Props 或 State
- 定义 API 请求/响应的数据结构
- 定义配置对象或选项
- 使用联合类型或交叉类型
- 定义函数式编程中的代数数据类型
选择"否"的情况:
- 这个情况实际上很少见,因为大部分非契约场景都是数据相关的
实际应用示例
让我们通过一些实际例子来应用这个决策树:
示例 1:定义数据库 Repository
问题:你在定义一个类的契约吗?
回答:是的,Repository 是一个类的契约
问题:要被多个类实现吗?
回答:是的,可能有 MySQLRepository、PostgreSQLRepository 等实现
结论:使用 interfacetypescript
interface UserRepository {
findById(id: number): Promise<User>;
save(user: User): Promise<User>;
}示例 2:定义 React 组件 Props
问题:你在定义一个类的契约吗?
回答:不是,Props 是数据结构
问题:是数据组合或配置吗?
回答:是的,Props 是组件的配置对象
结论:使用 typetypescript
type ButtonProps = {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick: () => void;
};示例 3:定义 API 响应
问题:你在定义一个类的契约吗?
回答:不是,API 响应是数据结构
问题:是数据组合或配置吗?
回答:是的,API 响应是数据结构
结论:使用 typetypescript
type UserResponse = {
id: number;
name: string;
email: string;
createdAt: string;
};示例 4:定义日志记录器
问题:你在定义一个类的契约吗?
回答:是的,Logger 是一个类的契约
问题:要被多个类实现吗?
回答:是的,可能有 ConsoleLogger、FileLogger、RemoteLogger 等实现
结论:使用 interfacetypescript
interface Logger {
log(message: string): void;
error(message: string): void;
}特殊情况处理
有些情况下,决策树可能不能直接给出答案,这时我们需要考虑以下因素:
团队约定
如果团队已经有明确的约定,应该优先遵循团队规范。例如,如果团队约定所有类型都使用 type,那就应该遵守。
生态系统惯例
某些生态系统有自己的惯例。例如,在 React 生态中,组件 Props 通常使用 type;在 Node.js 服务端开发中,服务契约通常使用 interface。
未来扩展性
如果预期类型将来可能需要扩展,可以考虑使用 interface,即使当前只有一个实现。
小结
在本章中,我们提供了一个实用的决策树来帮助选择 interface 和 type:
- 首先判断是否在定义类的契约
- 如果是契约,再判断是否需要多实现
- 如果不是契约,基本上都应该使用
type
这个决策树并不是绝对的规则,而是一个指导原则。在实际应用中,还需要考虑团队约定、生态系统惯例等因素。
下一章,我们将探讨 TypeScript 类型设计的哲学,帮助你从根本上理解如何设计良好的类型系统。
思考题: 你能用这个决策树分析一下你在项目中定义的一个类型应该使用 interface 还是 type 吗?