- 对于普通对象形状,接口和类型别名几乎可以互换。
- 只有
type可以命名联合、元组和其他非对象类型。 - 只有
interface支持声明合并,其中两个声明合并。 - 一个共同的规则:对象形状的接口,
type其他一切的接口,一致地应用。
很少有 TypeScript 问题能像 TypeScript 接口与类型 那样引起如此多的争论。许多文章列出了过去在旧版本中很重要但现在已经过时的差异。因此,我们没有重复声明,而是对其进行了测试。
我们编写了一个文件来验证每个差异,并在严格模式下使用 TypeScript 5.6.3 进行检查。下面引用的编译器错误来自该运行。版本之间的行为可能会发生变化,因此请检查您的项目使用的 TypeScript 版本。如果您是该语言的新手,请从我们的 TypeScript 初学者教程 开始。
简短的回答
interface 描述了对象的形状,并且可以扩展或重新打开。 type 别名为 任何 类型提供名称,包括对象、联合、元组和函数类型。两者都可以工作,区别主要在于风格。
它们哪里相同
这两个声明描述了相同的形状:
interface User {
id: number;
name: string;
email?: string;
}
type UserAlias = {
id: number;
name: string;
email?: string;
};
const a: User = { id: 1, name: "Asha" };
const b: UserAlias = a; // no error
TypeScript 使用结构类型,这意味着它比较形状而不是名称。这就是为什么 User 可以毫无怨言地分配给 UserAlias。两者都支持可选属性、readonly属性、方法和索引签名。
接口与类型并排
| 特点 | 接口 | 类型 |
|---|---|---|
| 物体形状 | 是的 | 是的 |
工会,例如 "a" | "b" |
否 | 是的 |
| 元组和基元 | 否 | 是的 |
| 映射类型和条件类型 | 否 | 是的 |
| 声明合并 | 是的 | 不,这是一个错误 |
| 延伸 | 延伸 |
& 交叉口 |
| 冲突的属性 | 声明错误 | 可以悄然成为never |
与 implements 一起使用 |
是的 | 是的,如果它是对象类型 |
只有打字才能做什么
许多有用的类型根本不是对象形状,只有类型别名才能命名它们:
type Status = "pending" | "paid" | "shipped"; // union
type Point = [number, number]; // tuple
type Handler = (event: string) => void; // function type
type Keys = keyof User; // computed type
type ReadonlyUser = Readonly<User>; // utility type
联合类型是 TypeScript 最有价值的功能之一,因为编译器会拒绝列表之外的任何值:
let s: Status = "paid";
s = "refunded";
// error TS2322: Type '"refunded"' is not assignable to type 'Status'.
这可以在编译时捕获拼写错误和过时的值,然后再到达生产环境。
声明合并
如果您两次声明同一个名称的接口,TypeScript 会将它们合并为一个:
interface Settings {
theme: string;
}
interface Settings {
language: string;
}
const settings: Settings = { theme: "dark", language: "en" }; // no error
使用类型别名尝试同样的事情,编译器会拒绝:
type Config = { theme: string };
type Config = { language: string };
// error TS2300: Duplicate identifier 'Config'.
合并是库让您扩展其类型的方式。例如,您可以将自定义属性添加到全局 Window 接口或框架的请求对象,而无需修改库。这确实有用。
另一方面是合并可能会意外发生。如果项目中的两个文件在同一范围内声明一个具有相同名称的接口,它们会默默地合并,这可能会造成混乱。相反,类型别名会使该冲突成为错误。
延伸与交叉点
两者都允许您在现有类型的基础上进行构建:
interface Admin extends User {
permissions: string[];
}
type AdminAlias = UserAlias & { permissions: string[] };
对于兼容的属性,结果是相同的。差异表现为冲突。当接口尝试以不兼容的方式更改属性的类型时,编译器会立即报告:
interface Base { id: number }
interface Broken extends Base { id: string }
// error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
// Types of property 'id' are incompatible.
// Type 'string' is not assignable to type 'number'.
交叉点在声明时不会抱怨。相反,冲突的属性变为 never,这是任何值都不能拥有的类型。该错误仅在稍后您尝试使用它时出现:
type Mixed = { id: number } & { id: string };
const m: Mixed = { id: 1 };
// error TS2322: Type 'number' is not assignable to type 'never'.
后来的错误更令人困惑,因为它偏离了实际的错误。这是在构建对象类型时更喜欢 extends 的实际原因之一。
类和工具
类可以承诺使用 implements 来匹配形状,这适用于接口或对象类型别名:
class Account implements User {
constructor(public id: number, public name: string) {}
}
这编译没有错误。请注意,implements 仅检查类是否匹配。它不添加任何属性或代码。类无法实现联合类型,因为没有单一的形状可以匹配。
只读和实用类型
Readonly、Partial、Pick 和 Omit 等实用程序类型可与接口和别名一起使用。结果是一个新类型,通常用类型别名命名:
const r: ReadonlyUser = { id: 3, name: "Dev" };
r.name = "Chitra";
// error TS2540: Cannot assign to 'name' because it is a read-only property.
请记住,readonly 只是编译时检查。在运行时,该对象是普通的 JavaScript,并且仍然可以通过绕过类型的代码进行更改。
提示: 当 TypeScript 编译为 JavaScript 时,类型消失。从外部到达的数据(例如 API 响应)不会仅仅因为它具有类型而在运行时进行检查。例如使用模式库对其进行验证。我们的 Fetch API 教程 涵盖了安全处理 API 响应的内容。
编译器性能
TypeScript 团队自己的性能指南建议优先使用使用 extends 的接口而不是大型交叉点,因为编译器可以更有效地缓存接口关系。在中小型项目中,您不太可能注意到任何差异。对于具有深度组合类型的非常大的代码库来说,这可能很重要。我们自己没有测量这个。
你应该使用哪个?
根据 TypeScript 手册的指导,广泛使用的规则是:
- 将
interface用于对象形状,尤其是公共 API 以及类实现或其他可能扩展的任何内容。 - 对联合、元组、函数类型以及映射或计算类型使用
type。
| 你正在定义 | 使用 |
|---|---|
| 数据模型,例如用户或订单 | 接口 |
| 一组允许值 | type 联盟 |
| 固定长度对或三元组 | type 元组 |
| 回调签名 | 类型 |
| 扩展库的类型 | interface、通过合并 |
| 现有类型的变体 | type 具有实用类型 |
有些团队只是在各处使用 type,这也很有效。最重要的是一致性,所以选择一个约定,将其写在你的风格指南中,并让 linter 强制执行它。 typescript-eslint 中的 ESLint consistent-type-definitions 规则可以做到这一点。
常见错误
- 无休止的争论。 两者对于对象来说都很好,所以确定一次规则然后继续前进。
- 意外合并。 在同一范围内重复使用接口名称会默默地组合它们。
- 冲突的交叉点。他们稍后创建
never种带有令人困惑的错误的类型。 - 运行时信任类型。它们会被删除,因此请验证外部数据。
- 使用
any消除错误。它关闭检查。更喜欢unknown并缩小范围。
在现有代码库中在它们之间切换
将 interface 更改为 type 或相反,对于普通对象形状通常是安全的,因为 TypeScript 通过结构而不是名称来比较类型。在切换之前仍需要检查一些事项:
- 将接口转换为类型别名会删除声明合并,因此任何具有相同名称的第二个声明都会成为重复标识符错误。
- 将类型别名转换为接口仅适用于对象形状。联合、元组和映射类型必须保留为类型别名。
- 错误消息和编辑器悬停可能会以不同的方式显示类型,这可能会让队友感到惊讶,但不会改变行为。
在一个集中的拉取请求中进行更改,在严格模式和测试套件下运行编译器,并避免将其与功能工作混合,以便审阅者可以看到没有其他任何更改。
最后的话
接口与类型问题有一个冷静的答案:它们对于对象重叠,每个都有一些独特的功能,并且一致性比选择更重要。除非您的团队更喜欢其他方式,否则请使用形状和类型的界面来处理其他一切。要亲自查看差异,请将上面的示例复制到文件中并对其运行 tsc --noEmit --strict。
常见问题
TypeScript 中的接口和类型有什么区别?
两者都可以描述物体形状。只有类型可以命名联合、元组和计算类型,而只有接口支持声明合并。
我应该为 React props 使用接口还是类型?
两者都效果很好。许多团队使用接口作为 props,而其他团队则更喜欢使用类型以与联合保持一致。选择其中之一并将其应用到整个项目中。
类型可以扩展接口吗?
是的,通过使用交集,例如类型 Admin = User & { requests: string[] }。接口还可以扩展对象类型别名。
接口比类型快吗?
TypeScript 的性能指南有利于接口扩展,而不是大型代码库中的大型交叉点。在典型的项目中,差异不太可能明显。
什么是声明合并?
这是指同一范围内具有相同名称的两个接口合并为一个。类型别名无法合并,重复的名称会导致错误。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →