Интерфейсы представляют собой способ описать объект без указания его реализации. Для их объявления в 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) для описания таких подходов, как “Адаптер”, “Фабрика” или “Декоратор” и др.