TypeScript 定义类型更应该用 type 还是 interface?

本文翻译自 Type vs Interface: Which Should You Use

自 TypeScript 第一个版本开始 interface 就存在,它们受到面向对象编程的启发,允许你定义对象或使用继承来创建类型。 但 TypeScript 中的 type 除了表示 string、number 等基本类型,也可以定义对象甚至通过交叉类型联合类型 来表达类型间的关系并创建一个新类型。 声明对象类型时,更应该使用 type 还是 interface 呢?

快速总结

应该默认使用 type,直到您需要 interface 的特定功能,例如 extends

  • 接口不能表达联合、映射类型或条件类型,类型别名可以表达任何类型。
  • 接口可以使用 extends,类型不能。
  • 当使用相互继承的对象时,请使用接口。extends 使 TypeScript 的类型检查器运行速度比使用联合类型稍快。
  • 同一范围内同名的接口会合并其声明,从而导致意外的错误。
  • 类型别名具有隐式索引签名 Record<PropertyKey, unknown>,但 interface 没有 - 这偶尔会引起一些类型匹配问题。

使用接口进行对象继承

如果要处理相互继承的对象,请使用接口。虽然我们可以用交叉类型类表达继承关系但不太理想,原因与 TypeScript 检查类型的速度有关。

使用 extends 创建接口时,TypeScript 可以按其名称在内部注册表中缓存该接口。这意味着将来可以更快地对其进行检查。 对于交叉类型,则不能通过名称缓存它,几乎每次都需要进行类型计算。 这是一个小的优化,但在大型项目中这个影响会逐步累积,这就是为什么 TypeScript 性能 wiki 建议使用接口进行对象继承的原因。

但是,我仍然不建议您默认使用接口,为什么?

接口可以声明合并

接口还有另一个特点,如果你没有做好准备,可能会感到非常惊讶:当在同一范围内声明两个同名的接口时,它们会合并它们的声明。

如果你尝试使用类型来实现这一点,那么将会报错,这是预期的行为和必要的语言功能。 它用于对修改全局对象的 JavaScript 库进行建模,例如向 string 原型添加方法。 但如果您对此没有做好准备,它可能会导致真正令人困惑的错误。 如果你想避免这种情况,我建议你将 ESLint 添加到你的项目中并启用该 no-redeclare 规则。

类型与接口中的索引签名

接口和类型之间的另一个区别很微妙。

类型别名具有隐式索引签名,但接口没有。这意味着它们可以分配给具有索引签名的类型,但接口不能。这可能会导致 类型 "x" 中缺少类型 "string" 这样的索引签名错误。

interface KnownAttributes {
  x: number;
  y: number;
}

const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};

type RecordType = Record<string, number>;

const oi: RecordType = knownAttributes;
// Type 'KnownAttributes' is not assignable to type 'RecordType'.
// Index signature for type 'string' is missing in type 'KnownAttributes'.

出现此错误的原因是接口稍后可能会被扩展。它可能添加了与 string 的键或 number 的值不匹配的属性。

您可以通过在接口中添加显式索引签名来解决此问题:

interface KnownAttributes {
  x: number;
  y: number;
  [index: string]: unknown; // new!
}

或者简单地将其改为使用type:

type KnownAttributes = {
  x: number;
  y: number;
};

const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};

type RecordType = Record<string, number>;

const oi: RecordType = knownAttributes;

这不是很奇怪吗!

默认用 type,而不是 interface

TypeScript 文档对此有一个很好的指南。他们涵盖了每个功能(尽管没有隐式索引签名),但他们得出的结论与我不同。

他们建议您根据个人喜好进行选择,我同意这一点。type 和 interface 之间的差异很小,因此您可以毫无问题地使用其中任何一种。

但是 TS 团队建议您默认使用 interface,并且仅在需要时使用 type。

我建议相反的做法。声明合并和隐式索引签名的功能足够令人惊讶,它们应该吓得你不要默认使用接口。

对于对象继承,我仍然推荐使用 interface,但我建议你默认使用 type,它更灵活,也更不令人惊讶。