Type or Interface?看这篇就够了!
今天是坚持日更的第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",
wrongProperty: 123,
// 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",
wrongProperty: 123,
// 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 = {
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'.
这个错误的原因是接口后面可以被扩展。它可能会增加一个不匹配字符串键或数字值的属性。
您可以通过为接口添加显式的索引签名来修复此错误:
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;
默认使用 type ,而不是 interface
TypeScript 文档对此有一个很好的指。它们涵盖了每个特性(虽然没有隐式索引签名),它们建议你根据个人喜好进行选择,type
和 interface
之间的区别很小,你可以使用其中之一而几乎没有太多问题。
但 TypeScript 团队建议您默认使用 interface
,并且仅在需要时才使用 type
。但是个人建议在实现对象继承时,推荐使用 interface
,在声明合并和隐式索引签名的功能时,建议使用 type
。
大家都在看