typescript中interface与type有何区别
区别
interface 和 type 都可以用来声明和约束变量类型结构,interface 可以被继承重载,type 要想被继承只能使用联合&,而且 interface 的性能比 type 更好
As of TypeScript 3.2 (Nov 2018), the following is true:
Aspect | Type | Interface |
---|---|---|
Can describe functions | ✅ | ✅ |
Can describe constructors | ✅ | ✅ |
Can describe tuples | ✅ | ✅ |
Interfaces can extend it | ⚠️ | ✅ |
Classes can extend it | 🚫 | ✅ |
Classes can implement it (implements ) | ⚠️ | ✅ |
Can intersect another one of its kind | ✅ | ⚠️ |
Can create a union with another one of its kind | ✅ | 🚫 |
Can be used to create mapped types | ✅ | 🚫 |
Can be mapped over with mapped types | ✅ | ✅ |
Expands in error messages and logs | ✅ | 🚫 |
Can be augmented | 🚫 | ✅ |
Can be recursive | ⚠️ | ✅ |
何时使用type
:
type
在定义基本类型(字符串、布尔值、数字、bigint、符号等)的别名时使用type
定义元组类型时使用type
定义函数类型时使用type
定义联合时使用type
当尝试通过组合重载对象类型中的函数时使用type
当需要利用映射类型时使用
何时使用interface
:
- 用于不需要
interface
使用的所有对象类型(见上文)type
interface
当您想要利用声明合并时使用。
原始类型
type
和之间最容易看到的区别interface
是只能type
用于给原始类型起别名:
type Nullish = null | undefined;
type Fruit = 'apple' | 'pear' | 'orange';
type Num = number | bigint;
这些示例都无法通过接口来实现。
💡 为原始值提供类型别名时,请使用关键字type
。
元组类型
元组只能通过关键字输入type
:
type row = [colOne: number, colTwo: string];
💡 为元组提供类型时使用type
关键字。
功能类型
type
函数可以通过和关键字输入interface
:
// via type
type Sum = (x: number, y: number) => number;
// via interface
interface Sum {
(x: number, y: number): number;
}
由于两种方式都可以达到相同的效果,因此规则将在这些场景中使用,type
因为它更容易阅读(并且不那么冗长)。
type
💡在定义函数类型时使用。
联合类型
联合类型只能通过type
关键字来实现:
type Fruit = 'apple' | 'pear' | 'orange';
type Vegetable = 'broccoli' | 'carrot' | 'lettuce';
// 'apple' | 'pear' | 'orange' | 'broccoli' | 'carrot' | 'lettuce';
type HealthyFoods = Fruit | Vegetable;
💡 定义联合类型时,使用type
关键字
对象类型
JavaScript 中的对象是键/值映射, "object type" 在TypeScript中总是使用键/值映射的方式。正如原始问题所明确的那样,在为对象提供类型时都可以使用interface
和。type
那么什么时候使用type
vsinterface
来表示对象类型呢?
交集与继承
通过类型和组合,我可以做这样的事情:
interface NumLogger {
log: (val: number) => void;
}
type StrAndNumLogger = NumLogger & {
log: (val: string) => void;
}
const logger: StrAndNumLogger = {
log: (val: string | number) => console.log(val)
}
logger.log(1)
logger.log('hi')
Typescript 非常高兴。如果我尝试用接口扩展它呢:
interface StrAndNumLogger extends NumLogger {
log: (val: string) => void;
};
这个声明StrAndNumLogger
给了我一个错误:
Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'
对于接口,子类型必须与超类型中声明的类型完全匹配,否则 TS 将抛出类似上面的错误。
💡 当尝试重载对象类型中的函数时,最好使用关键字type
。
声明合并
Typescript 中的接口与类型的关键区别在于,它们在声明后可以使用新功能进行扩展。当您想要扩展从节点模块导出的类型时,会出现此功能的常见用例。例如,@types/jest
导出使用 jest 库时可以使用的类型。然而,jest 还允许使用新功能扩展主jest
类型。例如,我可以添加这样的自定义测试:
jest.timedTest = async (testName, wrappedTest, timeout) =>
test(
testName,
async () => {
const start = Date.now();
await wrappedTest(mockTrack);
const end = Date.now();
console.log(`elapsed time in ms: ${end - start}`);
},
timeout
);
然后我可以这样使用它:
test.timedTest('this is my custom test', () => {
expect(true).toBe(true);
});
现在,一旦测试完成,该测试所用的时间将打印到控制台。伟大的!只有一个问题 - typescript 不知道我添加了一个timedTest
函数,所以它会在编辑器中抛出一个错误(代码会正常运行,但 TS 会生气)。
为了解决这个问题,我需要告诉 TS,除了 jest 已经提供的现有类型之外,还有一个新类型。为此,我可以这样做:
declare namespace jest {
interface It {
timedTest: (name: string, fn: (mockTrack: Mock) => any, timeout?: number) => void;
}
}
由于接口的工作方式,此类型声明将与从 导出的类型声明合并@types/jest
。所以我不只是重新声明jest.It
;我扩展jest.It
了一个新函数,以便 TS 现在知道我的自定义测试函数。
这种类型的事情是不可能用type
关键字实现的。如果@types/jest
使用关键字声明它们的类型type
,我将无法使用我自己的自定义类型扩展这些类型,因此没有好的方法让 TS 对我的新函数感到满意。这个关键字特有的过程interface
称为声明合并。
声明合并也可以在本地进行,如下所示:
interface Person {
name: string;
}
interface Person {
age: number;
}
// no error
const person: Person = {
name: 'Mark',
age: 25
};
如果我使用关键字执行与上面完全相同的操作type
,我会收到错误,因为类型无法重新声明/合并。在现实世界中,JavaScript 对象很像这个interface
例子;它们可以在运行时用新字段动态更新。
💡 因为接口声明可以合并,所以接口比类型更准确地表示 JavaScript 对象的动态性质,因此应该首选它们。
映射的对象类型
通过type
关键字,我可以利用这样的映射类型:
type Fruit = 'apple' | 'orange' | 'banana';
type FruitCount = {
[key in Fruit]: number;
}
const fruits: FruitCount = {
apple: 2,
orange: 3,
banana: 4
};
这不能通过接口来完成:
type Fruit = 'apple' | 'orange' | 'banana';
// ERROR:
interface FruitCount {
[key in Fruit]: number;
}
💡 当需要利用映射类型时,使用type
关键字
表现
大多数时候,对象类型的简单类型别名的作用与接口非常相似。
interface Foo { prop: string }
type Bar = { prop: string };
但是,一旦您需要组合两个或多个类型,您就可以选择使用接口扩展这些类型,或者在类型别名中将它们相交,此时差异就开始变得重要了。
Interface 创建一个单一的平面对象类型来检测属性冲突,这通常对于解决很重要!另一方面,交集只是递归地合并属性,并且在某些情况下不会产生任何结果。界面也始终显示得更好,而交叉点的类型别名无法显示在其他交叉点的部分中。接口之间的类型关系也被缓存,而不是作为一个整体的交集类型。最后一个值得注意的区别是,在检查目标交叉点类型时,在检查“有效”/“扁平”类型之前先检查每个成分。
因此,建议使用接口/扩展来扩展类型,而不是创建交集类型。