查看原文
其他

Type or Interface?看这篇就够了!

小懒 FED实验室 2024-02-12
关注下方公众号,获取更多热点资讯

今天是坚持日更的第135天,如果本文对您有帮助,请在文章末尾点击分享、在看、点赞支持我


在日常开发中,如果你的项目使用过 TypeScript,是不是总能看到 interface 和 type 的身影。本文将深入探讨 interface 和 type 的区别,以便在开发中合理的选择和使用。

默认情况下,您应该使用 type,直到您需要 interface 的特定功能(如 "extends"),主要区别如下:

  • interface 无法表达联合类型、映射类型或条件类型。type 别名可以表达任何类型。
  • interface 可以使用 extends,而 type 不能。
  • extends 可以让 TypeScript 的类型检查程序运行得比使用 & 更快。
  • 在同一作用域中具有相同名称的 interface 会合并其声明,可能导致意想不到的错误。
  • 类型别名的隐式索引签名为 Record<PropertyKey,unknown>,偶尔会出现这种情况。

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

interface WithId {
  id: string;
}
 
interface User extends WithId {
  name: string;
}
 
const user: User = {
  id"123",
  name"Karl",
  wrongProperty123,
// Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};

然而,它们内置了一个替代方法——使用 type 关键字声明的类型别名。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",
  wrongProperty123,
  // Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};

上面的代码很好,但它并不是最优的。原因在于 TypeScript 检查类型的速度。

当你使用 extends 创建接口时,TypeScript 可以在内部注册表中缓存接口的名称。这意味着将来针对它的检查可以更快。而对于使用 & 的交叉类型,TypeScript 无法通过名称来缓存它--它几乎每次都要计算它。

这只是一个小的优化,但如果接口被多次使用,它会累积。这就是为什么 TypeScript 性能 wiki 文章中建议使用接口进行对象继承的原因。

然而,仍然不建议您默认使用接口。

接口可以声明合并

接口还有另一个特性,当在同一作用域中声明了两个同名接口时,它们的声明就会合并:

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 库建模,例如为字符串原型添加方法。如果你想增加对这种情况的约束,建议你在项目中添加 ESLint,并打开 no-redeclare 规则。

类型与接口中的索引签名

接口与类型之间还有一个微妙的区别。类型别名具有隐式索引签名,但接口没有。这意味着它们可以赋值给有索引签名的类型,但接口没有。这可能导致以下错误:

interface KnownAttributes {
  x: number;
  y: number;
}
 
const knownAttributes: KnownAttributes = {
  x1,
  y2,
};
 
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'.

这个错误的原因是接口后面可以被扩展。它可能会增加一个不匹配字符串键或数字值的属性。

您可以通过为接口添加显式的索引签名来修复此错误:

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

或者简单地将其更改为使用类型声明:

type KnownAttributes = {
  x: number;
  y: number;
};
 
const knownAttributes: KnownAttributes = {
  x1,
  y2,
};
 
type Recordtype = Record<string, number>;
 
const oi: Recordtype = knownAttributes;

默认使用 type ,而不是 interface

TypeScript 文档对此有一个很好的指。它们涵盖了每个特性(虽然没有隐式索引签名),它们建议你根据个人喜好进行选择,typeinterface 之间的区别很小,你可以使用其中之一而几乎没有太多问题。

但 TypeScript 团队建议您默认使用 interface,并且仅在需要时才使用 type。但是个人建议在实现对象继承时,推荐使用 interface,在声明合并和隐式索引签名的功能时,建议使用 type

大家都在看

继续滑动看下一个

Type or Interface?看这篇就够了!

小懒 FED实验室
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存