Skip to content

typescript中interface与type有何区别

区别

interface 和 type 都可以用来声明和约束变量类型结构,interface 可以被继承重载,type 要想被继承只能使用联合&,而且 interface 的性能比 type 更好

As of TypeScript 3.2 (Nov 2018), the following is true:

AspectTypeInterface
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用于给原始类型起别名:

js
type Nullish = null | undefined;
type Fruit = 'apple' | 'pear' | 'orange';
type Num = number | bigint;

这些示例都无法通过接口来实现。

💡 为原始值提供类型别名时,请使用关键字type

元组类型

元组只能通过关键字输入type

js
type row = [colOne: number, colTwo: string];

💡 为元组提供类型时使用type关键字。

功能类型

type函数可以通过和关键字输入interface

js
// via type
type Sum = (x: number, y: number) => number;

// via interface
interface Sum {
  (x: number, y: number): number;
}

由于两种方式都可以达到相同的效果,因此规则将在这些场景中使用,type因为它更容易阅读(并且不那么冗长)。

type💡在定义函数类型时使用。

联合类型

联合类型只能通过type关键字来实现:

js
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那么什么时候使用typevsinterface来表示对象类型呢?

交集与继承

通过类型和组合,我可以做这样的事情:

js
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 非常高兴。如果我尝试用接口扩展它呢:

js
interface StrAndNumLogger extends NumLogger { 
    log: (val: string) => void; 
};

这个声明StrAndNumLogger给了我一个错误

Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'

对于接口,子类型必须与超类型中声明的类型完全匹配,否则 TS 将抛出类似上面的错误。

💡 当尝试重载对象类型中的函数时,最好使用关键字type

声明合并

Typescript 中的接口与类型的关键区别在于,它们在声明后可以使用新功能进行扩展。当您想要扩展从节点模块导出的类型时,会出现此功能的常见用例。例如,@types/jest导出使用 jest 库时可以使用的类型。然而,jest 还允许使用新功能扩展主jest类型。例如,我可以添加这样的自定义测试:

js
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
  );

然后我可以这样使用它:

js
test.timedTest('this is my custom test', () => {
  expect(true).toBe(true);
});

现在,一旦测试完成,该测试所用的时间将打印到控制台。伟大的!只有一个问题 - typescript 不知道我添加了一个timedTest函数,所以它会在编辑器中抛出一个错误(代码会正常运行,但 TS 会生气)。

为了解决这个问题,我需要告诉 TS,除了 jest 已经提供的现有类型之外,还有一个新类型。为此,我可以这样做:

js
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称为声明合并

声明合并也可以在本地进行,如下所示:

js
interface Person {
  name: string;
}

interface Person {
  age: number;
}

// no error
const person: Person = {
  name: 'Mark',
  age: 25
};

如果我使用关键字执行与上面完全相同的操作type,我会收到错误,因为类型无法重新声明/合并。在现实世界中,JavaScript 对象很像这个interface例子;它们可以在运行时用新字段动态更新。

💡 因为接口声明可以合并,所以接口比类型更准确地表示 JavaScript 对象的动态性质,因此应该首选它们。

映射的对象类型

通过type关键字,我可以利用这样的映射类型:

js
type Fruit = 'apple' | 'orange' | 'banana';

type FruitCount = {
  [key in Fruit]: number;
}

const fruits: FruitCount = {
  apple: 2,
  orange: 3,
  banana: 4
};

这不能通过接口来完成:

js
type Fruit = 'apple' | 'orange' | 'banana';

// ERROR: 
interface FruitCount {
  [key in Fruit]: number;
}

💡 当需要利用映射类型时,使用type关键字

表现

大多数时候,对象类型的简单类型别名的作用与接口非常相似。

js
interface Foo { prop: string }

type Bar = { prop: string };

但是,一旦您需要组合两个或多个类型,您就可以选择使用接口扩展这些类型,或者在类型别名中将它们相交,此时差异就开始变得重要了。

Interface 创建一个单一的平面对象类型来检测属性冲突,这通常对于解决很重要!另一方面,交集只是递归地合并属性,并且在某些情况下不会产生任何结果。界面也始终显示得更好,而交叉点的类型别名无法显示在其他交叉点的部分中。接口之间的类型关系也被缓存,而不是作为一个整体的交集类型。最后一个值得注意的区别是,在检查目标交叉点类型时,在检查“有效”/“扁平”类型之前先检查每个成分。

因此,建议使用接口/扩展来扩展类型,而不是创建交集类型。

前端知识体系 · wcrane