在 TypeScript 编程中,type 和 interface 是两种不同的类型定义方式,它们各自有不同的使用场景和特点。翻译一篇文章《类型(Type)与接口(Interface):2023年应该使用哪个?》,希望大家对 type 及 interface 有更加深入的了解。
简明解释:
默认情况下,应该使用类型(types),直到你需要接口(interfaces)的特定功能,比如 extends
。
-
接口无法表示联合类型(unions)、映射类型(mapped types)或条件类型(conditional types)。类型别名(type aliases)可以表示任何类型。
-
接口可以使用
extends
,而类型无法。 -
当你在处理互相继承的对象时,请使用接口。使用
extends
可以使 TypeScript 的类型检查器比使用 '&' 运行得稍快。 -
在同一作用域内具有相同名称的接口会合并它们的声明,从而导致意想不到的错误。
-
类型别名在隐式索引签名方面具有
Record<PropertyKey, unknown>
,这会偶尔出现。
完整解释:
TypeScript 提供了一个一流的原语(primitive)来定义继承自其他对象的对象,即接口(interface)。
接口自 TypeScript 的第一个版本就存在了。它们受到面向对象编程的启发,允许您使用继承来创建类型:
javascript
interface WithId {
id: string;
}
interface User extends WithId {
name: string;
}
const user: User = {
id: "123",
name: "Karl",
wrongProperty: 123,
> Type '{ id: string; name: string; wrongProperty: number; }' is not assignable to type 'User'.
> Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};
然而,它们还有一个内置的替代方案 - 使用 type 关键字声明的类型别名(type aliases)。type 关键字可以用来表示 TypeScript 中的任何类型,不仅限于对象类型。
假设我们想要表示一种既可以是字符串也可以是数字的类型。我们无法使用接口来实现这一点,但是可以使用类型别名:
javascript
type StringOrNumber = string | number;
const func = (arg: StringOrNumber) => {};
func("hello");
func(123);
func(true);
> Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.
当然,类型别名也可以用来表示对象。这在 TypeScript 用户中引发了很多争论。在声明对象类型时,应该使用接口还是类型别名呢?
使用接口进行对象继承
如果您使用的对象是相互继承的,请使用接口。我们上面的示例,使用 WithId
,可以使用类型别名和交叉类型来表示。
javascript
type WithId = {
id: string;
};
type User = WithId & {
name: string;
};
const user: User = {
id: "123",
name: "Karl",
wrongProperty: 123,
> Type '{ id: string; name: string; wrongProperty: number; }' is not assignable to type 'User'.
> Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};
这是完全可以的代码,但略微不太优化。原因与 TypeScript 检查类型的速度有关。
当您使用 extends 创建一个接口时,TypeScript 可以通过其名称在内部注册表中缓存该接口。这意味着将来对它的检查可以更快地进行。而使用 & 的交叉类型时,它无法通过名称缓存它 - 它几乎每次都要计算。
这只是一个小的优化,但如果接口被多次使用,这些小的优化会累积起来。这就是为什么TypeScript 的性能维基 建议在对象继承方面使用接口 - 我也持这个观点。
然而,我仍然不建议默认使用接口。为什么呢?
接口可以声明合并
接口具有另一个特性,如果您没有准备好,可能会显得非常令人惊讶。
当在同一作用域内声明具有相同名称的两个接口时,它们会合并它们的声明。
javascript
interface User {
name: string;
}
interface User {
id: string;
}
const user: User = {
> Property 'name' is missing in type '{ id: string; }' but required in type 'User'.
id: "123",
};
如果您尝试在类型中这样做,它是不起作用的:
javascript
type User = {
> Duplicate identifier 'User'.
name: string;
};
type User = {
> Duplicate identifier 'User'.
id: string;
};
这是有意的行为,也是一项必要的语言特性。它用于模拟修改全局对象的 JavaScript 库,例如向 string
原型添加方法。
但是,如果您没有为此做好准备,它可能会导致非常令人困惑的错误。
如果您想避免这种情况,我建议您将 ESLint 添加到您的项目中,并启用 no-redeclare 规则。
类型与接口中的索引签名
接口和类型之间的另一个不同之处是一个微妙的地方。
类型别名具有隐式的索引签名,但接口没有。这意味着类型别名可以分配给具有索引签名的类型,但接口不能。这可能会导致如下错误:
字符串 "类型的索引签名在 "x "类型中丢失。
javascript
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
值不匹配的属性。
您可以通过向接口添加显式的索引签名来修复这个问题:
javascript
interface KnownAttributes {
x: number;
y: number;
[index: string]: unknown; // new!
}
或者简单地将其更改为使用类型别名:
javascript
type KnownAttributes = {
x: number;
y: number;
};
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
};
type RecordType = Record<string, number>;
const oi: RecordType = knownAttributes;
这不是很奇怪吗!
默认为类型,而不是接口
TypeScript 文档对此有很好的指南。他们涵盖了每个特性(尽管没有涵盖隐式索引签名),但得出的结论与我不同。
他们建议根据个人偏好进行选择,我也同意这个观点。类型(type)和接口(interface)之间的差异足够小,以至于您可以在不会遇到太多问题的情况下使用其中任何一个。
但 TypeScript 团队建议默认使用接口,只有在需要时才使用类型。我想提出相反的建议。声明合并和隐式索引签名的特性足够令人惊讶,以至于它们应该警告您不要默认使用接口。
对于对象继承,仍然推荐使用接口,但我建议您默认使用类型。类型(type)稍微更加灵活,也不会那么令人意外。