在TypeScript中,扩展接口和交叉接口有什么区别?

115

假设已经定义了以下类型:

interface Shape {
  color: string;
}

现在,考虑以下几种方法来向这种类型添加额外的属性:

扩展

interface Square extends Shape {
  sideLength: number;
}

交集

type Square = Shape & {
  sideLength: number;
}

这两种方法的区别是什么?

而且,为了完整起见和出于好奇,还有其他能够产生类似结果的方式吗?


3
另请参阅:接口与交集 - KyleMit
@KyleMit 文档只是说明了“两者之间的主要区别在于如何处理冲突”,但没有解释不同的冲突处理方式,最终并不够有用。下面的答案应该在文档中说明。https://github.com/microsoft/TypeScript-Website/blob/6accc6c7f5f14ed91f93d1afcbec9c05f7dc1669/packages/documentation/copy/en/handbook-v2/Object%20Types.md?plain=1#L459 我们可以提交一个PR :) - Luke
我刚刚给了你第100个赞! - Robo Robok
交叉类型和扩展类型的一个用途是:结合两个接口,例如 type NewInterface = FirstInterface & SecondInterface; - Zach Saucier
2个回答

131

是的,有一些差异可能与您的情况相关或不相关。

其中最显著的可能是当两种类型中都存在具有相同属性键的成员时,它们如何处理的差异。

请考虑:

interface NumberToStringConverter {
  convert: (value: number) => string;
}

interface BidirectionalStringNumberConverter extends NumberToStringConverter {
  convert: (value: string) => number;
}

上述的extends导致错误,因为派生接口声明了一个与派生接口中具有相同键但具有不兼容签名的属性。
error TS2430: Interface 'BidirectionalStringNumberConverter' incorrectly extends interface 'NumberToStringConverter'.

  Types of property 'convert' are incompatible.
      Type '(value: string) => number' is not assignable to type '(value: number) => string'.
          Types of parameters 'value' and 'value' are incompatible.
              Type 'number' is not assignable to type 'string'.

然而,如果我们使用交叉类型
type NumberToStringConverter = {
  convert: (value: number) => string;
}

type BidirectionalStringNumberConverter = NumberToStringConverter & {
  convert: (value: string) => number;
}

没有任何错误,而且更进一步给出
// And this is a good thing indeed as a value conforming to the type is easily conceived
const converter: BidirectionalStringNumberConverter = {
    convert: (value: string | number) => {
        return (typeof value === 'string' ? Number(value) : String(value)) as string & number; // type assertion is an unfortunately necessary hack.
    }
}

const s: string = converter.convert(0); // `convert`'s call signature comes from `NumberToStringConverter`

const n: number = converter.convert('a'); // `convert`'s call signature comes from `BidirectionalStringNumberConverter`

游乐场链接

这导致了另一个有趣的差异,接口声明是开放式的。新成员可以在任何地方添加,因为在同一声明空间中具有相同名称的多个接口声明会被合并。
下面是合并行为的常见用法。

lib.d.ts

interface Array<T> {
  // map, filter, etc.
}

array-flat-map-polyfill.ts

interface Array<T> {
  flatMap<R>(f: (x: T) => R[]): R[];
}

if (typeof Array.prototype.flatMap !== 'function') {
  Array.prototype.flatMap = function (f) { 
    // Implementation simplified for exposition. 
    return this.map(f).reduce((xs, ys) => [...xs, ...ys], []);
  }
}

注意到没有extends子句,尽管在不同的文件中指定了接口,但这些接口都在全局范围内,并按名称合并为一个单一的逻辑接口声明,其中包含两组成员。(对于模块作用域的声明,可以使用稍微不同的语法来完成相同的操作)
相比之下,存储在type声明中的交叉类型是封闭的,不会被合并。
有很多很多的区别。您可以在TypeScript手册中了解更多关于这两个结构的信息。Object TypesCreating Types from Types部分特别相关。

2
很棒的答案。感谢您指出“覆盖”属性时行为上的区别,我之前并不知道。这本身就是在某些情况下使用类型的好理由。您能指出接口合并有用的情况吗?在构建应用程序时是否有有效的用例(换句话说:不是库)? - Willem-Aart
1
@AluanHaddad,“StringToNumberConverter”类型应该改名为“BidirectionalStringNumberConverter”,对吗?看起来其他实例可能已经被重命名了... - Karl Horky
这还正确吗?我将您的示例插入到TypeScript Playground中,编译器报错了。 - Nathan Chappell
1
@NathanChappell 谢谢你发现了这个问题。我不知道它是什么时候出错的。我已经更新了示例以使其能够编译,但现在需要类型断言。我会进一步研究这个问题。 - Aluan Haddad
1
@AluanHaddad 谢谢。TS 似乎在快速变化,所以很可能无法跟上它(特别是因为他们似乎已经放弃了维护规范...) - Nathan Chappell
显示剩余3条评论

1

16
这句话实际上没有表达任何意思。我在手册中找到它,但是由于手册对此描述含糊不清,因此我需要通过谷歌来寻找答案。 - Robo Robok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接