Разница между расширением и пересечением интерфейсов в TypeScript?

#typescript #interface #extends #type-alias

#typescript #типы #пересечение #расширяет

Вопрос:

Допустим, определен следующий тип:

 interface Shape {
  color: string;
}
 

Теперь рассмотрим следующие способы добавления дополнительных свойств к этому типу:

Расширение

 interface Square extends Shape {
  sideLength: number;
}
 

Пересечение

 type Square = Shape amp; {
  sideLength: number;
}
 

В чем разница между обоими подходами?

И, для полноты картины и из любопытства, существуют ли другие способы получения сопоставимых результатов?

Комментарии:

1. См. Также : Интерфейсы против Пересечения

Ответ №1:

Да, есть различия, которые могут иметь или не иметь значения в вашем сценарии.

Возможно, наиболее существенным является различие в том, как обрабатываются элементы с одним и тем же ключом свойства, когда они присутствуют в обоих типах.

Рассмотрим:

 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 amp; {
  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 amp; 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`
 

Ссылка на игровую площадку

Это приводит к еще одному интересному отличию: interface объявления являются открытыми. Новые члены могут быть добавлены в любом месте, потому что несколько interface объявлений с одинаковым именем в одном пространстве объявлений объединяются.

Вот обычное использование для поведения слияния

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 . Раздел интерфейсов и расширенных типов особенно актуален.

Комментарии:

1. Отличный ответ. Спасибо, что указали на разницу в поведении при «переопределении» свойств, не знал об этом. Это само по себе является веской причиной для использования типов в определенных вариантах использования. Можете ли вы указать ситуации, в которых объединение интерфейсов полезно? Существуют ли допустимые варианты использования при создании приложений (другими словами: не библиотек)?

2. Willem Aart как вы предполагаете, это наиболее полезно для написания библиотек, но что такое приложение, если не набор библиотек (включая ваше собственное приложение). Это может быть чрезвычайно полезно и для приложений. Пример: interface Object {hasOwnProperty<T, K extends string>(this: T, key: K): this is {[P in K]?}} который превращается Object.prototype.hasOwnProperty в защиту типа, вводя для него дополнительную, более конкретную подпись. .

3. @AluanHaddad StringToNumberConverter вместо этого тип должен быть назван BidirectionalStringNumberConverter , правильно? Похоже, что другие экземпляры, возможно, были переименованы…

4. @NathanChappell спасибо, что уловили это. Я не знаю, когда это сломалось. Я обновил пример, чтобы он компилировался, но теперь для него требуется утверждение типа. Я рассмотрю это подробнее.

5. @AluanHaddad спасибо. Похоже, что TS меняется довольно быстро, поэтому, вероятно, невозможно идти в ногу с этим (тем более, что они, похоже, отказались от поддержания спецификации …)