interface vs type:选型指南
本教程共 80 篇 · 第 28 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:理清 interface 和 type 的本质差异,建立一个”什么时候用哪个”的判断框架,不再纠结于这个问题。
“interface 和 type 到底有什么区别?“——这大概是 TypeScript 社区被问得最多的问题之一。如果你去搜,会看到大量争论,有人站 interface、有人站 type,也有人觉得”都一样随便用”。
真相是:它们大部分时候确实可以互换,但在特定场景下各有优劣。 这一章的任务不是制造宗教战争,而是帮你建立一个清晰的判断框架。
先看一张全景对比图
| 维度 | interface | type |
|---|---|---|
| 定义对象形状 | ✅ 原生设计 | ✅ 可以 |
| 函数类型 | ✅ 调用签名 { (): R } | ✅ 箭头函数风格 |
| 联合类型 | ❌ | ✅ 核心优势 |
| 元组 | ❌ | ✅ 核心优势 |
| 原始类型别名 | ❌ | ✅ |
| 映射类型 | ❌ | ✅ 重点场景 |
| 条件类型 | ❌ | ✅ 重点场景 |
| 声明合并 | ✅ 核心优势 | ❌ |
| extends 冲突检测 | ✅ 编译时立即报错 | ❌ 延迟到 never |
| extends 继承 | ✅ | ❌ 用 & 替代 |
| 类 implements | ✅ | ✅ |
| 递归引用 | ✅ | ✅ |
现在逐条展开看这些差异在实际代码中意味着什么。
interface 的独门绝技
声明合并
这是 interface 和 type 最根本的差异。interface 同名声明会自动合并:
interface User {
name: string;
}
// 另一个地方(可能是另一个文件、另一段代码)
interface User {
age: number;
}
// 最终类型:{ name: string; age: number }
const u: User = { name: "Alice", age: 30 };
而 type 会直接报”重复标识符”错误。
Tip声明合并是一个强大的机制,但不建议在日常业务代码中刻意依赖它来拼凑类型。它的正当用途主要是”给第三方库的类型声明打补丁”(module augmentation),或者 TypeScript 标准库内部用来扩展内置类型(比如每次新标准给
Array加方法时)。
extends 属性冲突即时检测
第 25 章已经演示过:当 extends 的两个父接口有同名但类型不兼容的属性时,编译直接报错。而用 & 做交叉类型时,不兼容的属性类型会被悄悄收窄成 never,直到你尝试构造对象时才暴露问题。
这不是”谁好谁坏”的问题,而是两种设计哲学:
extends偏向 fail-fast——尽早告诉你出问题了。&偏向 容忍——先接受,用的时候再判断。
对于定义对象形状这件事,fail-fast 通常更友好。
对”对象描述”有优化
TypeScript 编译器内部对 interface 有专门优化——检查对象是否匹配 interface 的速度比检查 type 更快。这个差异在绝大多数项目里感知不到,但如果你的类型定义量非常大(比如大型 Monorepo),interface 确实会快那么一点。TypeScript 官方团队也明确表示 interface 是描述对象的首选。
type 的独门绝技
联合类型和元组
// 这些 interface 完全做不了
type Status = "pending" | "success" | "error";
type Point3D = [number, number, number];
type Mixed = string | { code: number; message: string };
联合类型和元组是 type 最常用的场景。如果你的类型需要”要么这样、要么那样”的语义,那 type 是唯一选择。
映射类型
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
映射类型只能通过 type 定义。虽然 TypeScript 内置了很多工具类型(Partial、Readonly、Pick 等等),但理解底层原理能让你在需要自定义映射时不至于束手无策。
条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
条件类型是 TypeScript 类型系统的”if-else”,也是只能通过 type 定义。条件类型配合泛型和 infer 关键字,可以实现非常复杂的类型逻辑——不过那属于进阶内容了。
社区的演进:从”interface 优先”到”各取所需”
早期 TypeScript(2014-2019 年左右)的社区共识非常明确:能用 interface 就用 interface,除非你需要 type 的专属能力。
这个共识的来源很合理:
- 那时候 type 的功能还没那么丰富。
- interface 的错误信息更好读。
- 官方文档也推荐 interface。
但随着 TypeScript 发展,type 的能力不断增强,很多团队(尤其是 React 社区)开始全面使用 type。他们的理由也很充分:
- type 语法更简洁(不需要分号,可以用
&和|)。 - 在一个文件里统一用 type,不需要同时学两套语法。
- type 能做所有事情,不需要纠结”这个场景该用哪个”。
我们的建议:TypeScript 7.0 选型策略
回到 2026 年的 TypeScript 7.0,结合当前生态和最佳实践,我们的建议是:
策略一:能用 interface 优先(推荐大多数团队)
- 描述纯对象形状和类的契约时优先用
interface。 - 需要联合类型、元组、映射类型、条件类型时用
type。 - 对外暴露的公共 API 用
interface(方便消费者通过声明合并扩展)。
这条策略简单好记,适合多成员协作的团队——因为规则清晰,不会出现”这边用 type、那边用 interface”的风格混战。
策略二:全用 type(适合偏好一致性的小团队)
- 如果你觉得两条规则也是一种心智负担,那全部用
type也不是不行。 - 代价是牺牲声明合并能力和 extends 的即时冲突检测。
不推荐的策略:全用 interface。 因为 interface 做不了联合类型、元组、映射类型,你最后一定还是需要 type——所以”全用 interface”是个伪命题。
一句话总结
// 对象形状 → interface
interface User {
name: string;
age: number;
}
// 联合 / 元组 / 映射 / 条件 → type
type Status = "active" | "inactive";
type Point = [number, number];
type PartialUser = Partial<User>;
团队协作中的实际考量
如果你的团队已经有既定规范(比如 ESLint 的 @typescript-eslint/consistent-type-definitions 规则),那就遵守团队规范。技术选型的价值在于一致性——团队里所有人都按同一套规则写,读代码的成本最低。
如果你是从零开始定规范,建议这样配置:
{
"rules": {
"@typescript-eslint/consistent-type-definitions": ["error", "interface"]
}
}
这条规则强制所有对象类型定义使用 interface。那遇到必须用 type 的情况怎么办?放心——这条规则只检查可互换的场景(纯对象形状),当你写了联合类型或元组时它不会拦你。
小结
- interface 和 type 在描述纯对象形状时可以互换,但背后的机制不同。
- interface 的优势:声明合并、extends 冲突即时检测、编译器优化。
- type 的优势:联合类型、元组、映射类型、条件类型。
- 推荐策略:“能用 interface 优先,需联合 / 映射 / 条件时用 type”。
- 核心原则:团队一致性 > 个人偏好。遵守项目既定规范比纠结哪个”更好”重要得多。
搞懂了选型逻辑,最后一章我们来聊聊声明合并——这个让 interface 如此独特的机制到底是怎么运作的,以及为什么它既是利器也是陷阱。