TypeScript 中扩展接口和交叉接口之间的区别?

假设定义了以下类型:

interface Shape {
color: string;
}

现在,考虑以下方法向此类型添加其他属性:

分机

interface Square extends Shape {
sideLength: number;
}

十字路口

type Square = Shape & {
sideLength: number;
}

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

而且,出于完整性和好奇心,还有其他方法产生可比较的结果吗?

21982 次浏览

Yes there are differences which may or may not be relevant in your scenario.

Perhaps the most significant is the difference in how members with the same property key are handled when present in both types.

Consider:

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


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

The extends above results in an error because the derriving interface declares a property with the same key as one in the derived interface but with an incompatible signature.

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'.

However, if we employ intersection types

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


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

There is no error whatsoever and further given

// 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`

Playground Link

This leads to another interesting difference, interface declarations are open ended. New members can be added anywhere because multiple interface declarations with same name in the same declaration space are merged.

Here is a common use for merging behavior

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], []);
}
}

Notice how no extends clause is present, although specified in separate files the interfaces are both in the global scope and are merged by name into a single logical interface declaration that has both sets of members. (the same can be done for module scoped declarations with slightly different syntax)

By contrast, intersection types, as stored in a type declaration, are closed, not subject to merging.

There are many, many differences. You can read more about both constructs in the TypeScript Handbook. The Interfaces and Advanced Types section are particularly relevant.

Typescript has updated their documentation with a section describing this scenario!

https://www.typescriptlang.org/docs/handbook/2/objects.html#interfaces-vs-intersections

"The principle difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type."