TypeScript 定义类型更应该用 type 还是 interface?
自 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,它更灵活,也更不令人惊讶。