Pull to refresh

Интерфейсы в TypeScript

Level of difficultyEasy

Интерфейсы представляют собой способ описать объект без указания его реализации. Для их объявления в Typescript используется ключевое слово interface. То есть интерфейс служит для записи формы объекта и может использоваться для указания типа переменной. Этой переменной можно присвоить любой объект, подходящий под описание интерфейса. Также интерфейсы можно расширять с помощью ключевого слова extends аналогично классам, но делать это стоит с осторожностью и не создавать больших иерархий.

interface IButton {
  text: string,   
  click: () => void;
}

В данном примере мы объявили интерфейс IButton, который требует, чтобы у объекта было строковое поле text и функция click, которая ничего не принимает и не возвращает. В разных языках программирования существуют разные соглашения по именованию интерфейсов, но для удобства понимания, что это интерфейс я буду именовать их с большой буквы I. Теперь мы можем создать объект, подходящий под это описание и присваивать его переменной типа интерфейса. Причём, если мы пропустим хоть одно описание из интерфейса или добавим лишнего, то линтер TS предупредит нас об этом даже до компиляции:

/* Property 'click' is missing in type '{ text: string; }' 
but required in type 'IButton' */
const primaryButton: IButton = {text: 'Подтвердить'} 

/* Object literal may only specify known properties, 
and 'id' does not exist in type 'IButton' */
const primaryButton: IButton = {
  text: 'Подтвердить',   
  id: 'main',   
  click: () => {       console.log('Отправка формы');   }
}

Таким образом, интерфейсы - это помощник компилятора и разработчика, которые служат средством проверки формы объекта до этапа выполнения. После компиляции в JS они просто удаляются, так как больше не нужны.

У интерфейсов есть ещё одно полезное свойство: они могут выступать ещё и контрактами, которые должен будет выполнять класс. Причём класс может взять на себя выполнение сразу нескольких контрактов с помощью ключевого слова implements:

class PrimaryButton implements IButton, IHidable {   
  constructor(text: string) {       
    this.text = text;   
  }   
  // Члены интерфейса IButton   
  text: string;   click(): void {       
    console.log('Отправка формы');   
  }   
  // Члены интерфейса IHidable   
  hide(): void {       
    console.log('Скрываем кнопку');   
  }   
  show(): void {       
    console.log('Показываем кнопку');   
  }
}

Реализовав интерфейс, класс говорит о том, что он соблюдает контракт интерфейса и объекты этого класса можно присвоить переменной типа этого интерфейса:

const btn: IButton = new PrimaryButton('Ок');

И тут мы подходим к тому, что интерфейсы являются отличным инструментом для полиморфизма (несколько классов, реализующих один интерфейс и имеющих различающийся функционал, могут быть взаимозаменяемы в моменте использования). Например, у нас есть несколько разных кнопок, которые надо скрыть в определённый момент и мы их можем присвоить однотипному массиву и выполнить общий метод hide на всех кнопках:

class ImageButton implements IButton, IHidable {   
  constructor(text: string, imgUrl: string) {       
    this.text = text;   
  }   
  // Дополнительные члены   
  private setImage(imgUrl: string) {       
    // TODO: Установка фона картинки   
  }   
  // Члены интерфейса IButton   
  text: string;   
  click(): void {       
    console.log('Отправка формы');   
  }   
  // Члены интерфейса IHidable   
  hide(): void {       
    console.log('Скрываем кнопку');   
  }   
  show(): void {       
    console.log('Показываем кнопку');   
  }
}

const btn1 = new PrimaryButton('Ок');
const btn2 = new ImageButton('Продолжить', 'https://some-url.ru/next.png');
const buttons: IHidable[] = [btn1, btn2];
buttons.forEach(b => b.hide());

Но формы объектов описывают не только с помощью ключевого слова interface, а также и ключевым словом type. Так в чём же отличие interface от type? На самом деле это очень похожие вещи и отличий почти нет, но всё же несколько интерфейсов с одним именем в одной области автоматически сольются в один общий интерфейс, а type не могут. Интерфейсы не могут использовать выражения & или | при объявлении, а type могут. А также при расширении (extends) интерфейсов TS проверит совместимость членов интерфейсов, а при расширении type такой проверки не будет:

interface IA {   
  go(n: number): string;
}
/* Interface 'IB' incorrectly extends interface 'IA'.   
The types returned by 'go(...)' are incompatible between these types.     
Type 'number' is not assignable to type 'string'. */
interface IB extends IA {   
  go(n: number): number;   
  stop(n: number): number;
}

// =================================================

type A = {   
  go(n: number): string;
}
// Без ошибок
type B = A & {   go(n: number): number;}

Интерфейсы в TS применяются повсюду: их используют для описания типов возвращаемых объектов из сетевых сервисов, сложных параметров функции, а также возвращаемых значений. Но этим их применение не исчерпывается, так как они играют роль абстракции и применяются для возможности расширяемости кода без его изменения, а также для инверсии зависимостей из принципов SOLID. 

Также интерфейсы упрощают реализации паттернов программирования “Банды четырёх” (GoF) для описания таких подходов, как “Адаптер”, “Фабрика” или “Декоратор” и др.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.