likes
comments
collection
share

【译】类型(Type)与接口(Interface):2023年应该使用哪个?

作者站长头像
站长
· 阅读数 9

在 TypeScript 编程中,type 和 interface 是两种不同的类型定义方式,它们各自有不同的使用场景和特点。翻译一篇文章《类型(Type)与接口(Interface):2023年应该使用哪个?》,希望大家对 type 及 interface 有更加深入的了解。

源文地址

【译】类型(Type)与接口(Interface):2023年应该使用哪个?

简明解释:

默认情况下,应该使用类型(types),直到你需要接口(interfaces)的特定功能,比如 extends

  • 接口无法表示联合类型(unions)、映射类型(mapped types)或条件类型(conditional types)。类型别名(type aliases)可以表示任何类型。

  • 接口可以使用 extends,而类型无法。

  • 当你在处理互相继承的对象时,请使用接口。使用 extends 可以使 TypeScript 的类型检查器比使用 '&' 运行得稍快。

  • 在同一作用域内具有相同名称的接口会合并它们的声明,从而导致意想不到的错误。

  • 类型别名在隐式索引签名方面具有 Record<PropertyKey, unknown>,这会偶尔出现。

完整解释:

TypeScript 提供了一个一流的原语(primitive)来定义继承自其他对象的对象,即接口(interface)。

接口自 TypeScript 的第一个版本就存在了。它们受到面向对象编程的启发,允许您使用继承来创建类型:


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 中的任何类型,不仅限于对象类型。

假设我们想要表示一种既可以是字符串也可以是数字的类型。我们无法使用接口来实现这一点,但是可以使用类型别名:


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,可以使用类型别名和交叉类型来表示。


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 的性能维基 建议在对象继承方面使用接口 - 我也持这个观点。

然而,我仍然不建议默认使用接口。为什么呢?

接口可以声明合并

接口具有另一个特性,如果您没有准备好,可能会显得非常令人惊讶。

当在同一作用域内声明具有相同名称的两个接口时,它们会合并它们的声明。


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",
};

如果您尝试在类型中这样做,它是不起作用的:


type User = {
> Duplicate identifier 'User'.
  name: string;
};
 
type User = {
> Duplicate identifier 'User'.
  id: string;
};

这是有意的行为,也是一项必要的语言特性。它用于模拟修改全局对象的 JavaScript 库,例如向 string 原型添加方法。

但是,如果您没有为此做好准备,它可能会导致非常令人困惑的错误。

如果您想避免这种情况,我建议您将 ESLint 添加到您的项目中,并启用 no-redeclare 规则。

类型与接口中的索引签名

接口和类型之间的另一个不同之处是一个微妙的地方。

类型别名具有隐式的索引签名,但接口没有。这意味着类型别名可以分配给具有索引签名的类型,但接口不能。这可能会导致如下错误:

字符串 "类型的索引签名在 "x "类型中丢失。


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 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)稍微更加灵活,也不会那么令人意外。