TypeScript 接口与类型:真正的差异,使用编译器进行测试

  1. 首页
  2. 博客
  3. 软件开发

软件开发

TypeScript 接口与类型:使用编译器测试的真正差异

接口和类型别名重叠太多,以至于选择常常让人感觉随意。以下是每个人可以做而另一个人不能做的事情,通过真实的编译器错误以及您的团队可以遵循的简单规则来确认。

阅读约 8 分钟
TypeScript 接口与类型:真正的差异,用编译器测试

要点速览

  • 对于普通对象形状,接口和类型别名几乎可以互换。
  • 只有 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 的性能指南有利于接口扩展,而不是大型代码库中的大型交叉点。在典型的项目中,差异不太可能明显。

什么是声明合并?

这是指同一范围内具有相同名称的两个接口合并为一个。类型别名无法合并,重复的名称会导致错误。

继续学习

与Stack Quora合作

需要测试、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%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker 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 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →