Interface js что это
Интерфейс определяет свойства и методы, которые объект должен реализовать. Другими словами, интерфейс — это определение кастомного типа данных, но без реализации. В данном случае интерфейсы в TS похожи на интерфейсы в языках Java и C#. Интерфейсы определяются с помощью ключевого слова interface . Для начала определим простенький интерфейс:
interface IUser
Интерфейс в фигурных скобках определяет два свойства: id, которое имеет тип number, и name, которая представляет строку. Теперь используем его в программе:
let employee: IUser = < id: 1, name: "Tom" >console.log("id: ", employee.id); console.log("name: ", employee.name);
По сути employee — обычный объект за тем исключением, что он имеет тип IUser . Если правильнее говорить, то employee реализует интерфейс IUser. Причем эта реализация накладывает на employee некоторые ограничения. Так, employee должен реализовать все свойства и методы интерфейса IUser, поэтому при определении employee данный объект обязательно должен включать в себя свойства id и name.
Параметры методов и функций также могут представлять интерфейсы:
interface IUser < id: number; name: string; >let employee: IUser = < id: 1, name: "Alice" >function printUser(user: IUser): void < console.log("id: ", user.id); console.log("name: ", user.name) >printUser(employee);
В этом случае аргумент, который передается в функцию, должен представлять объект или класс, который реализует соответствующий интерфейс.
И также можно возвращать объекты интерфейса:
interface IUser < id: number; name: string; >function buildUser(userId: number, userName: string): IUser < return < id: userId, name: userName >; > let newUser = buildUser(2, "Bill"); console.log("id: ", newUser.id); console.log("name: ", newUser.name);
Необязательные свойства и свойства только для чтения
При определении интерфейса мы можем задать некоторые свойства как необязательные с помощью знака вопроса. Подобные свойства реализовать необязательно:
interface IUser < id: number; name: string; age?: number; >let employee: IUser = < id: 1, name: "Alice", age: 23 >let manager: IUser =
Свойство age помечено как необязательное, поэтому его можно не определять в объектах.
Также интерфейс может содержать свойства только для чтения, значение которых нельзя изменять. Такие свойства определяются с помощью ключевого слова readonly :
interface Point < readonly x: number; readonly y: number; >let p: Point = < x: 10, y: 20 >; console.log(p); // p.x = 5; // Ошибка - свойство доступно только для чтения
Определение методов
Кроме свойств интерфейсы могут определять функции:
interface IUser < id: number; name: string; sayWords(words: string): void; >let employee: IUser = < id: 1, name: "Alice", sayWords: function(words: string): void< console.log(`$говорит "$"`); > > employee.sayWords("Привет, как дела?");
Опять же объект, который реализует интерфейс, также обязан реализовать определенную в интерфейсе функцию с тем же набором параметров и тем типом выходного результата. В данном случае функция sayWords() в качестве параметра принимает строку и ничего возвращает, выводя на консоль некоторое сообщение.
Интерфейсы классов
Интерфейсы могут быть реализованы не только объектами, но и классами. Для этого используется ключевое слово implements :
interface IUser < id: number; name: string; getFullName(surname: string): string; >class User implements IUser < id: number; name: string; age: number; constructor(userId: number, userName: string, userAge: number) < this.id = userId; this.name = userName; this.age = userAge; >getFullName(surname: string): string < return this.name + " " + surname; >> let tom = new User(1, "Tom", 23); console.log(tom.getFullName("Simpson"));
Класс User реализует интерфейс IUser. В этом случае класс User обязан определить все те же свойства и функции, которые есть в IUser.
При этом объект tom является как объектом User, так и объектом IUser:
let tom :IUser = new User(1, "Tom", 23); //или let tom :User = new User(1, "Tom", 23);
Расширение интерфейса
TypeScript позволяет добавлять в интерфейс новые поля и методы, просто объявив интерфейс с тем же именем и определив в нем необходимые поля и методы. Например:
interface IUser < id: number; name: string; >interface IUser < age: number; >let employee: IUser = < id: 1, name: "Alice", age: 31 >function printUser(user: IUser): void < console.log(`id: $name: $ age: $`); > printUser(employee);
В данном случае первое определение интерфейса IUser содержит поля id и name . Второе определение интерфейса содержит объявление поля age . В итоге объект или класс, который реализует этот интерфейс, должен определить все три поля — id, name и age.
Наследование интерфейсов
Интерфейсы, как и классы, могут наследоваться:
interface IMovable < speed: number; move(): void; >interface ICar extends IMovable < fill(): void; >class Car implements ICar < speed: number; move(): void < console.log("Машина едет со скоростью " + this.speed + " км/ч"); >fill(): void < console.log("Заправляем машину топливом"); >> let auto = new Car(); auto.speed = 60; auto.fill(); auto.move();
После наследования интерфейс ICar будет также иметь все те свойства и функции, которые определены в IMovable. И тогда, класс Car, реализующий интерфейс ICar, должен будет реализовать также и свойства и методы интерфейса IMovable.
Интерфейсы функций
Интерфейсы функций содержат определение типа функции. Затем они должны быть реализованы объектом, который представляет функцию данного типа:
interface FullNameBuilder < (name: string, surname: string): string; >let simpleBuilder: FullNameBuilder = function (name:string, surname: string): string < return "Mr. " + name + " " + surname; >let fullName = simpleBuilder("Bob", "Simpson"); console.log(fullName); // Mr. Bob Simpson
Здесь определен интерфейс FullNameBuilder, который лишь содержит сигнатуру функции. Далее определяется переменная simpleBuilder, которая имеет тип FullNameBuilder и поэтому должна представлять функцию с данной сигнатурой.
Интерфейсы массивов
Интерфейсы массивов описывают объекты, к которым можно обращаться по индексу, как, например, к массивам
interface StringArray < [index: number]: string; >let phones: StringArray; phones = ["iPhone 7", "HTC 10", "HP Elite x3"]; let myPhone: string = phones[0]; console.log(myPhone);
Здесь определен интерфейс StringArray, который содержит сигнатуру массива. Эта сигнатура указывает, что объект, который реализует StringArray, может индексироваться с помощью чисел (объекта типа number). И, кроме того, данный объект должен хранить объекты типа string , то есть строки.
Выше индекс представлял тип number. Но мы можем использовать для индексации и тип string:
interface Dictionary < [index: string]: string; >var colors: Dictionary = <>; colors["red"] = "#ff0000"; colors["green"] = "#00ff00"; colors["blue"] = "#0000ff"; console.log(colors["red"]);
Гибридные интерфейсы
Интерфейсы могут сочетать различные стили, могут применяться сразу как к определению объекта, так и к определению функции:
interface PersonInfo < (name: string, surname: string):void; fullName: string; password: string; authenticate(): void; >function personBuilder(): PersonInfo < let person = function (name: string, surname: string): void< person.fullName = name + " " + surname; >; person.authenticate = function () < console.log(person.fullName + " входит в систему с паролем " + person.password); >; return person; > let tom = personBuilder(); tom("Tom", "Simpson"); tom.password = "qwerty"; tom.authenticate();
Тип функции, определяемый в таком гибридном интерфейсе, как правило, выступает в роли конструктора объекта. В данном случае такой конструктор имеет тип (name: string, surname: string):void; .
А функция, которая представляет данный интерфейс (в данном случае — функция personBuilder ), реализует эту функцию конструктора, и также может использовать другие свойства и методы, которые были определены в интерфейсе.
Интерфейсы и типы TypeScript: в чём разница
Помимо базовых типов, TypeScript предоставляет разработчикам операторы для определения интерфейсов ( interface ) и псевдонимов для типов ( type ). В актуальных версиях TypeScript эти операторы во многих случаях взаимозаменяемы. Может сложиться впечатление, что кроме написания никаких различий между ними нет. На самом деле это не так. Разберёмся на примерах.
Псевдонимы типов
Оператор type позволяет определить новый псевдоним для существующего типа. Слово «псевдоним» используется неслучайно. Оператор фактически добавляет дополнительное имя для существующего типа. Новый тип данных при этом не создаётся. Взгляните на пример:
type OrderNumber = string;
Мы определили псевдоним типа OrderNumber . Фактически это дополнительное имя для типа string . При объявлении переменной типа OrderNumber , нам не придётся делать каких-то преобразований при попытке записать в неё строковое значение. Ведь OrderNumber — дополнительное имя для типа string :
const myOrderNumber: OrderNumber = '31337';
Запомните, оператор type не создаёт новый тип, а добавляет псевдоним. Мы специально делаем на этом акцент, так как в различной литературе и статьях при обсуждении оператора type используют словосочетание «новый тип». Это сделано для удобства повествования. Фактически новый тип не создаётся.
⭐ Узнайте больше о теории типов, научитесь на практике использовать аннотацию типов и обобщённое программирование на профессиональном курсе.
Взаимозаменяемость
Во многих ситуациях type и interface взаимозаменяемы. Неважно каким оператором воспользуетесь, больших отличий сразу не заметите. С одной стороны — это хорошо, с другой вводит путаницу. Рассмотрим на примере определение псевдонима типа для описания заказа в импровизированном интернет-магазине. С точки зрения кода, заказ удобно описать в виде объекта определённой формы. Воспользуемся псевдонимами типов:
type Order = < id: string; createdAt: Date; items: string[]; >const myOrder: Order =
Сначала мы определяем псевдоним типа, а затем демонстрируем его применение. Объект myOrder должен соответствовать структуре Order и если забыть определить какое-то свойство, компилятор сразу напомнит об этом.

Аналогичного результата можно добиться при помощи интерфейсов. Вместо оператора type воспользуемся interface :
interface Order
Пример, где определяется объект заказа типа Order остаётся без изменений. Получается, что одну и ту же задачу мы решили разными способами. Какому оператору отдавать предпочтение? В последних версиях TypeScript грань между операторами interface и type стала тонкой (пример выше хорошо иллюстрирует это), но разница всё же есть.
Слияние интерфейсов
Интерфейсы поддерживают декларативное слияние, а псевдонимы типов нет. Объявив два или более интерфейса с одинаковыми идентификаторами (именами), мы получим один общий интерфейс:
interface Order < id: string; createdAt: Date; items: string[]; >interface Order < status: string; >interface Order < owner: string; >const myOrder: Order =
Мы определили три интерфейса с одинаковым идентификатором ( Order ), а затем воспользовались им для определения нового заказа. Обратите внимание, это полностью валидный код. Никаких ошибок о дублировании идентификатора нет. TypeScript выполнил слияние и на выходе получился один интерфейс Order .
Слияние интерфейсов может показаться бессмысленной возможностью, но это не так. Лучше всего она себя проявляет при разработке библиотек. Разработчик получает возможность расширять интерфейс без внесения изменений в исходное определение. Клиенту всегда важен более гибкий интерфейс.
А что на счёт псевдонимов? Они так не умеют. При определении двух псевдонимов с одинаковыми идентификаторами (именами) возникнет ошибка: «Duplicate identifier Order».

Типы пересечения
При разработке на TypeScript часто возникает необходимость комбинировать различные типы. Один из вариантов комбинации — пересечение типов. Результатом пересечения становится тип с общими характеристиками типов, участвующих в пересечении.
Если сказать проще, при пересечении типов A и B , мы получим тип C . Ему соответствуют значения, которые одновременно принадлежат к типам A и B , то есть обладают обязательными характеристиками каждого типа.

В TypeScript эту задачу решает оператор амперсанд ( & ). Рассмотрим на примере.
type OrderIdentifier = < id: string; >type OrderStatus = < status: string; >type Order = OrderIdentifier & OrderStatus;
За счёт пересечения мы получаем новый тип Order . Он объединяет общие характеристики типов OrderIdentifier и OrderStatus . Получить новый тип пересечения возможно и на основании интерфейсов:
interface OrderIdentifier < id: string; >interface OrderStatus < status: string; >type Order = OrderIdentifier & OrderStatus;
Результат выполнения этого кода не отличается от предыдущего. Однако, при объединении интерфейсов мы получаем тип пересечения Order . Обратите внимание, мы получаем именно тип пересечения, а не интерфейс. Получить новый интерфейс пересечения не получится. Это ещё одно отличие между type и interface .
Типы объединения
Псевдонимы типов можно объединять. В результате объединения получается новый тип. Новый тип содержит всё что есть в типах, участвующих в объединении. Для объединения применяется оператор | . Рассмотрим на примере:
type Dog = < bark: () =>void; > type Cat = < meow: () =>void; > type Animal = Dog | Cat; // Собака Гуффи const goofy: Dog = < bark() < console.log('bark'); >> // Кот Том const tom: Cat = < meow() < console.log('meow'); >> // Animal подойдёт для кошек let animal: Animal = tom; // и для собак animal = goofy;

Объединять можно не только псевдонимы, но и интерфейсы. Принцип тот же, что и при пересечении. Результатом станет новый псевдоним типа, объединяющий интерфейсы. Получить новый интерфейс в результате объединения не получится.
Интерфейсы и классы
Интерфейсы особенно удобны при использовании объектно-ориентированного подхода. Сначала проектируется интерфейс, а потом классы, которые его имплементируют. Для этого в TypeScript есть отдельная синтаксическая конструкция implements . Рассмотрим на примере:
interface Cat < meow: () =>void; > class Tiger implements Cat < meow() < console.log('meow-meow-bark'); >>
Мы описали интерфейс Cat , а затем определили класс Tiger . Этот класс реализует интерфейс Cat . Обратите внимание на ключевое слово implements . Приведённый пример кода ещё одна демонстрация, что интерфейс можно заменить на псевдоним типа. Например, так:
type Cat = < meow: () =>void; >
Классы могут имплементировать псевдоним типа, поэтому пример с определением класса Tiger остаётся актуальным, а мы опять увидели взаимозаменяемость type и interface .
Пожалуй, можно переходить к следующему разделу, но у интерфейсов есть ещё один козырь в рукаве. Интерфейсы поддерживают наследование. Работает это точно так же, как и в классах. При наследовании интерфейсов применяется оператор extends :
interface Cat < meow: () =>void; > interface FastCat extends Cat < run: () =>void; > class Tom implements FastCat < meow() < console.log('meow'); >run() < console.log('run'); >>
Интерфейс FastCat наследуется от интерфейса Cat . Таким образом, интерфейс FastCat включает всё, что есть в определении Cat . При имплементации интерфейса FastCat , класс должен реализовать оба метода meow и run .
Это ещё не всё. В качестве родителя для интерфейса может выступать класс. Новый интерфейс будет содержать поля и методы класса, а также то, что разработчик добавит в интерфейс. Наследуя интерфейс от класса, помните, что класс в этом случае не должен содержать приватных полей. Рассмотрим пример:
class Animal < walk() < console.log('walk'); >run() < console.log('run'); >> // Интерфейс включает методы // класса Animal interface Cat extends Animal < meow: () =>void; > class HomeCat implements Cat < walk() < console.log('Cat can walk'); >run() < console.log('Cat can run'); >meow() < console.log('meow'); >>
Рассматривать пример следует с описания класса Animal . Обычный класс с двумя методами: walk и run . Затем мы определяем новый интерфейс Cat . Он наследуется от класса Animal , следовательно, новый интерфейс Cat включает контракт для методов walk , run (результат наследования) и meow .
Для проверки интерфейса определяем новый класс HomeCat . Он имплементирует интерфейс Cat , который в свою очередь наследуется от класса Animal . Это означает, что класс HomeCat должен имлементировать все три метода.
Псевдонимы типов не поддерживают наследования. Оно возможно только с интерфейсами. Это ещё одно различие между этими конструкциями.
Кортежи
TypeScript поддерживает кортежи. Кортеж — упорядоченный набор фиксированной длины. Кортежи похожи на массивы, даже синтаксис используется такой же. Но в отличии от последних, кортеж не может динамически расширяться, и типы значений, а также их количество известны заранее. В следующем примере объявляем новый кортеж Developer :
type Developer = [string, string, number]; const ivan: Developer = ['Ivan', 'Ivanov', 33];
Объявить новый кортеж с помощью interface нельзя. Это ещё одна ситуация, когда операторы не взаимозаменяемы. Стоит добавить, что внутри интерфейса определять кортежи можно:
interface Developer < name: string; top2skill: [string, string]; >const ivan: Developer =
Резюме
Во многих случаях операторы type и interface взаимозаменяемы. Мы убедились в этом на практике. Мы рассмотрели несколько ситуаций, когда поведение отличается и использование одного оператора вместо другого невозможно. А что делать с ситуациями, когда допустимы оба оператора?
Дать объективный ответ на этот вопрос сложно. Всё зависит от нескольких «но» и взглядов разработчика. Уместны оба варианта. Однако, мы рекомендуем по умолчанию применять type . Оператор interface актуален во время применения объектно-ориентированного подхода, а также при создании библиотек и готовых пакетов с компонентами.
В случае с ООП всё относительно понятно: интерфейсы часть этого мира. Фраза «реализовать интерфейс» разработчику привычней по сравнению с «реализовать тип». С этим трудно поспорить. К тому же вы помните про наследование интерфейсов. Эта возможность может оказаться полезной.
Интерфейсы также пригодятся при разработке библиотеки или универсальных компонентов. В первую очередь из-за возможности бесшовного расширения. При необходимости разработчик, который применяет библиотеку, может расширить нужный интерфейс. Для этого достаточно объявить интерфейс с тем же идентификатором.
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.
TypeScript. Интерфейсы
В предыдущей статье по TypeScript мы познакомились с основными типами данных языка, а так же с возможностью создавать собственные примитивные типы.
Как гласит документация к языку, одним из основных принципов TypeScript является то, что типизация основана на структуре (shape) объектов. Такой способ типизации называют неявной или «утиной» — объект относят к тому или иному типу (классу, интерфейсу), если он имеет (реализует) все его свойства и методы. Интерфейсы в TS применяются как раз для того, чтобы описывать нужные вам типы.
Например, у нас есть функция callPet:
function callPet (pet) < console.log(`Hey, $
Она принимает в качестве аргумента объект питомца. Опишем его с помощью интерфейса:
interface Pet < name: string; age: number; color: string; >
Питомец имеет имя, возраст и окрас.
Теперь укажем, что callPet принимает именно объект питомца. И вызовем функцию:
function callPet (pet: Pet): void < console.log(`Hey, $
Заметим, что объект питомца создан “на месте”. Нигде не указано, что он реализует интерфейс Pet, проверка происходит только на соответствие свойств в самой функции callPet.
Если в передаваемый объект добавить свойство, которого нет в интерфейсе или, наоборот, убрать одно из описанных, компилятор TS выдаст ошибку о несоответствии типов. Это и есть неявная или «утиная» типизация.
Этот пример показывает основное назначение и использование интерфейсов в TS, далее мы рассмотрим другие возможности по работе с собственными типами в TypeScript.
Реализация интерфейсов
Как и во многих языках программирования, поддерживающих парадигму ООП, классы в TS могут “реализовывать” (implements) интерфейсы. Продолжим работать с интерфейсом Pet и опишем его поля более подробно:
interface Pet < name: string; readonly age: number; readonly color: string; readonly bday: Date; toy?: string; >
Возраст и окрас питомца не зависит от внешних факторов, поэтому сделаем эти свойства readonly. Если попытаться записать в такие свойства значения, компилятор выдаст ошибку. Так же добавим свойство bday, чтобы потом иметь возможность вычислить возраст. Свойство toy обозначено как необязательное с помощью ?. Поэтому если не указать его в классе, реализующем интерфейс, код скомпилируется без ошибок.
Теперь реализуем интерфейс Pet классом Dog:
class Dog implements Pet < name: string; readonly color: string; readonly bday: Date; constructor (name: string, color: string, bday: Date) < this.name = name; this.color = color; this.bday = bday; >get age() < const diff = new Date(new Date().getTime() - this.bday.getTime()); return diff.getFullYear() - new Date(0).getFullYear(); >> const bobby = new Dog(‘Bobby’, ‘black’, new Date(2010, 4, 12));
Если не реализовать в классе какое-либо свойство интерфейса, компилятор выдаст ошибку.
Note: Наличие модификатора readonly не проверяется, поэтому его нужно не забывать указывать самостоятельно. Для методов-геттеров он учитывается по умолчанию.
Методы
Помимо свойств, интерфейсы могут описывать методы классов:
interface Pet < speak(): void; run(meters: number): void; bringToy?(toy?: string): string; >class Dog implements Pet < speak(): void < console.log('Bark!'); >run(meters: number): void < console.log(`$
Наследование интерфейсов
Интерфейсы в TS могут наследовать друг друга. Например, мы можем сделать Dog не классом, а интерфейсом с полями, которые будут дополнять интерфейс Pet:
interface Dog implements Pet < readonly breed: string; >
Стоит заметить, что одновременно могут существовать и класс Dog, и интерфейс Dog. TypeScript сам разрешит конфликт имен при компиляции, поскольку интерфейсы в скомпилированный .js файл не входят.
Реализацию интерфейса Dog теперь будет выполнять классы для каждой породы. Чтобы не писать все методы заново, можно просто наследовать класс от класса Dog и реализовать им интерфейс Dog:
class GermanShepherd extends Dog implements Dog < readonly breed: string = 'German Shepherd'; >class Dalmatian extends Dog implements Dog < /* При указании значения свойства сразу при объявении, можно не указывать тип */ readonly breed = 'Dalmatian'; >const rex = new GermmanShepherd(‘Rex’, ‘black’, new Date(2014, 2, 10)); const pongo = new Dalmatian(‘Pongo’, ‘dotted’, new Date(2012, 7, 3));
Интерфейсы могут наследовать и классы. При наследовании от класса, интерфейс наследует все его свойства и методы (в том числе private и protected), но не наследует их значение и реализацию. Поскольку наследуются приватные свойства, такой интерфейс может реализовать только подкласс исходного класса.
Типизация массивов
Интерфейсы в TS позволяют задать статическую типизацию для индексируемых типов. Рассмотрим это на простом примере:
interface Shelter < [index: number]: Pet; >
Выше написано примерно следующее: тип Shelter это объект с числовыми индексами, по которым находятся объекты типа Pet. То есть Shelter, по сути, массив питомцев. Можно реализовывать индексацию самостоятельно, но гораздо проще наследовать класс от встроенного класса Array. Для того, чтобы компилятор понял, значения какого типа будут храниться в массиве, укажем его при наследовании: Array. Пример реализации интерфейса Shelter:
class DogShelter extends Array<Dog> implements Shelter < constructor(. dogs: Dog[]) < super(. dogs); >> const shelter = new DogShelter(bobby, rex, pongo); shelter.forEach(dog => dog.speak());
Множественная реализация
Класс может реализовывать больше одного интерфейса. Это бывает удобно, когда нужно логически разделить свойства сложного объекта. Например DogShelter помимо Shelter может реализовывать интерфейс Building:
interface Building < address: string; type: string; >class DogShelter extends Array<Dog> implements Shelter, Building < address: string; type: string; constructor(addess: string, . dogs: Dog[]) < super(. dogs); this.address = address; this.type = 'shelter' >>
Типизация функций
Интерфейсы могут быть использованы для того, чтобы описать функцию:
interface Build < (address: string, type: string): Building; >
Теперь можно строго определить тип функции с помощью созданного интерфейса:
let buildHouse: Build; buildHouse = function (address: string, type: string) < return
>; const WinterPalace = buildHouse(‘Palace Square’, ‘palace’);Гибридные типы
Интерфейс может совместить в себе описание и функции, и объекта, и индексируемого типа:
interface Build < /* Описание функции */ (name: string, address: string, type: string): Building; /* Описание полей объекта */ developer: string; started: Date; /* Индексируемый тип */ buildings: < [buildingName: string]: Building >> function DevelopingProject(developer: string, started: Date) < let project = <Build> function ( name: string, address: string, type: string ): Building < const building: Building =
; project.buildings[name] = building; return building; > project.buildings = <>; project.developer = developer; project.started = started; return project; > const SPb = DevelopingProject(‘Peter the Great’, new Date(1703)); const PeterAndPaulsFortress = SPb(‘Peter and Paul`s Fortress’, ‘Rabbit Island’, ‘fortress’);Главное
Интерфейсы в TS полезный и очень удобный инструмент для создания собственных типов данных. Они отлично подходят для описания классов и объектов и помогают избежать ошибок, связанных с неявными присваиваниями и преобразованиями.
С помощью интерфейсов, можно строго задавать структуру приложения. Например, популярный фреймворк Angular версии 2.0 использует интерфейсы, чтобы описывать жизненный цикл компонентов, а умные IDE умеют распознавать и подсказывать ошибки еще на уровне разработки, до компиляции.
Но важно помнить, что интерфейсы TS сильно отличаются от привычных интерфейсов ООП — они выполняют только функции пользовательских типов и не более. Именно поэтому они могут наследовать классы и другие интерфейсы.
If you like this article, share a link with your friends
Read more
We talk about interesting technologies and share our experience of using them.
Interfaces¶
Несмотря на то, что тема, относящаяся к интерфейсам, очень проста, именно она вызывает наибольшее количество вопросов у начинающих разработчиков. Поэтому такие вопросы как для чего нужны интерфейсы, когда их применять, а когда нет, будут подробно рассмотрены в этой главе.
Общая теория¶
По факту интерфейс затрагивает сразу несколько аспектов создания программ, относящихся к проектированию, реализации, конечной сборке. Поэтому, чтобы понять предназначение интерфейса, необходимо рассмотреть каждый аспект по отдельности.
Первый аспект (реализация) предлагает рассматривать создаваемые экземпляры как социальные объекты, чья публичная часть инфраструктуры была оговорена в контракте, к коему относится интерфейс. Другими словами, интерфейс — это контракт, реализация которого гарантирует наличие оговоренных в нем членов потребителю экземпляра. Поскольку интерфейс описывает исключительно типы членов объекта (поля, свойства, сигнатуры методов), они не могут гарантировать, что сопряженная с ними логика будет соответствовать каким-либо критериям. По этому случаю была принята методология, называемая контрактное программирование. Несмотря на то, что данная методология вызывает непонимание у большинства начинающих разработчиков, в действительности она очень проста. За этим таинственным термином скрываются рекомендации придерживаться устной или письменной спецификации при реализации логики, сопряженной с оговоренными в интерфейсе членами.
Второй аспект (проектирование) предлагает проектировать объекты менее независимыми за счет отказа от конкретных типов (классов) в пользу интерфейсов. Ведь пока тип переменной или параметра представляется классовым типом, невозможно будет присвоить значение, соответствующее этому типу, но не совместимое с ним. Под соответствующим подразумевается соответствие по всем обязательным признакам, но не состоящим в отношениях наследования. И, хотя в TypeScript из-за реализации номинативной типизации подобной проблемы не существует, по возможности рекомендуется придерживаться классических взглядов.
Третий аспект (сборка) вытекает из второго и предполагает уменьшение размера компилируемого пакета (bundle) за счет отказа от конкретных типов (классов). Фактически, если какой-либо объект требуется пакету лишь для выполнения операций над ним, последнему вовсе не нужно содержать определение первого. Другими словами, скомпилированный пакет не должен включать определение класса со всей его логикой только потому, что он указан в качестве типа. Для этого как нельзя лучше подходят типы, представленные интерфейсами. Хотя нельзя не упомянуть, что данная проблема не имеет никакого практического отношения к разработчикам на языке TypeScript, поскольку его (или точнее сказать JavaScript) модульная система лишена подобного недостатка.
Вот эти несколько строк, описывающие оговоренные в самом начале аспекты, заключают в себе ответы на все возможные вопросы, которые только могут возникнуть относительно темы, сопряженной с интерфейсами. Если ещё более доступно, то интерфейсы нужны для снижения зависимости и наложения обязательств на реализующие их классы. Интерфейсы стоит применять всегда и везде, где это возможно. Это не только повысит семантическую привлекательность кода, но и сделает его более поддерживаемым.
Нелишним будет добавить, что интерфейсы являются фундаментальной составляющей идеологии как типизированных языков, так и объектно-ориентированного программирования.
Такая известная группа программистов, как “Банда четырех” (Gang of Four, сокращённо GoF), в своей книге, положившей начало популяризации шаблонов проектирования, описывали интерфейс как ключевую концепцию объектно-ориентированного программирования (ооп). Понятие интерфейса является настолько важным, что в книге был сформулирован принцип объектно-ориентированного проектирования, который звучит так: Программируйте в соответствии с интерфейсом, а не с реализацией.
Другими словами, авторы советуют создавать систему, которой вообще ничего не будет известно о реализации. Проще говоря, создаваемая система должна быть построена на типах, определяемых интерфейсами, а не на типах, определяемых классами.
С теорией закончено. Осталось подробно рассмотреть реализацию интерфейсов в TypeScript.
Интерфейс в TypeScript¶
TypeScript предлагает новый тип данных, определяемый с помощью синтаксической конструкции, называемой интерфейс ( interface ).
Interface — это синтаксическая конструкция, предназначенная для описания открытой ( public ) части объекта без реализации (api). Хотя не будет лишним упомянуть, что существуют языки, позволяющие реализовывать в интерфейсах поведение, рассматриваемое как поведение по умолчанию.
Класс, реализующий интерфейс, обязан реализовать все описанные в нём члены. Поэтому интерфейс является гарантией наличия описанных в нем характеристик у реализующего его объекта. Все члены, описанные в интерфейсе, неявно имеют модификатор доступа public . Интерфейс предназначен для описания api или другими словами состояния и поведения, предназначенного для взаимодействия внешнего мира с объектом.
Объявление (declaration)¶
В TypeScript интерфейс объявляется с помощью ключевого слова interface , после которого указывается идентификатор (имя), за которым следует тело, заключенное в фигурные скобки, содержащее описание.
1 2 3
interface Identifier // тело интерфейса >
Объявление интерфейса возможно как в контексте модуля, так и в контексте функции или метода.
1 2 3 4 5 6 7 8 9 10 11
interface Identifier <> // контекст модуля class T public method(): void interface Identifier <> // контекст метода > > function func(): void interface Identifier <> // контекст функции >
Конвенции именования интерфейсов¶
Прежде чем продолжить, нужно обратить внимание на такой аспект, как конвенции именования интерфейсов. Существует два вида именования.
Первый вид конвенций родом из языка Java — они предлагают именовать интерфейсы точно так же, как и классы. Допускаются имена прилагательные.
interface Identifier <>
Второй вид предлагает использовать конвенции языка C#, по которым интерфейсы именуются так же, как классы, но с префиксом I , что является сокращением от Interface. Такой вид записи получил название “венгерская нотация” в честь программиста венгерского происхождения, работавшего в компании MicroSoft. Допускаются имена прилагательные.
interface IIdentifier <>
Чтобы сразу расставить все точки над i, стоит заметить, что в дальнейшем идентификаторы интерфейсов будут указываться по конвенциям C#.
Реализация интерфейса (implements)¶
Как уже было сказано в самом начале, все члены интерфейса являются открытыми ( public ), и их объявление не может содержать модификатор static . Кроме того, в TypeScript интерфейсы не могут содержать реализацию.
Класс, реализующий интерфейс, обязан реализовывать его в полной мере. Любой класс, который хочет реализовать интерфейс, должен указать это с помощью ключевого слова implements , после которого следует идентификатор реализуемого интерфейса. Указание реализации классом интерфейса располагается между идентификатором класса и его телом.
1 2 3 4 5 6 7 8 9 10 11
interface IAnimal nickname: string; execute(command: string): void; > class Bird implements IAnimal nickname: string; execute(command: string): void <> >
Один класс может реализовывать сколько угодно интерфейсов. В этом случае реализуемые интерфейсы должны быть перечислены через запятую.
1 2 3 4
interface IAnimal <> interface IOviparous <> // указывает на возможность откладывать яйца class Bird implements IAnimal, IOviparous <>
В случае, когда класс расширяет другой класс, указание реализации ( implements ) следует после указания расширения ( extends ).
1 2 3 4 5 6 7 8
interface IAnimal <> interface IOviparous <> class Bird implements IAnimal, IOviparous <> interface IFlyable <> class Eagle extends Bird implements IFlyable <>
Декларация свойств get и set (accessors)¶
Несмотря на то, что в интерфейсе можно декларировать поля и методы, в нем нельзя декларировать свойства get и set (аксессоры). Но, несмотря на это, задекларированное в интерфейсе поле может быть совместимо не только с полем, но и аксессорами. При этом нет разницы, будет в объекте объявлен getter, setter или оба одновременно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
interface IAnimal id: string; > // только get class Bird implements IAnimal get id(): string return 'bird'; > > // только set class Fish implements IAnimal set id(value: string) <> > // и get, и set class Insect implements IAnimal get id(): string return 'insect'; > set id(value: string) <> >
Указание интерфейса в качестве типа (interface types)¶
Класс, реализующий интерфейс, принадлежит к типу этого интерфейса. Класс, унаследованный от класса, реализующего интерфейс, также наследует принадлежность к реализуемым им интерфейсам. В подобных сценариях говорят, что класс наследует интерфейс.
1 2 3 4 5 6 7 8
interface IAnimal <> class Bird implements IAnimal <> class Raven extends Bird <> let bird: IAnimal = new Bird(); let raven: IAnimal = new Raven();
Класс, реализующий множество интерфейсов, принадлежит к типу каждого из них. Когда экземпляр класса, реализующего интерфейс, присваивают ссылке с типом интерфейса, то говорят, что экземпляр был ограничен типом интерфейса. То есть, функционал экземпляра класса урезается до описанного в интерфейсе (подробнее об этом речь пойдет в главе Совместимость объектов) и Совместимость функций).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
interface IAnimal name: string; > interface IFlyable flightHeight: number; > interface IIdentifiable id: string; > class Animal implements IAnimal constructor(readonly name: string) <> > class Bird extends Animal implements IFlyable public flightHeight: number = 500; > var animal: IAnimal = new Bird('bird'); // экземпляр Bird ограничен до типа IAnimal var fly: IFlyable = new Bird('bird'); // экземпляр Bird ограничен до типа IFlyable
Несмотря на то, что интерфейс является синтаксической конструкцией и может указываться в качестве типа, после компиляции от него не остается и следа. Это в свою очередь означает, что интерфейс, как тип данных, может использоваться только на этапе компиляции. Другими словами, компилятор сможет предупредить об ошибках несоответствия объекта описанному интерфейсу, но проверить на принадлежность к типу интерфейса с помощью операторов typeof или instanceof не получится, поскольку они выполняются во время выполнения программы. Но в TypeScript существует механизм (который будет рассмотрен далее в главе Защитники типа), позволяющий в некоторой мере решить эту проблему.
Расширение интерфейсов (extends interface)¶
Если множество логически связанных интерфейсов требуется объединить в один тип, то нужно воспользоваться механизмом расширения интерфейсов. Наследование интерфейсов осуществляется с помощью ключевого слова extends , после которого через запятую идет один или несколько идентификаторов расширяемых интерфейсов.
1 2 3 4 5 6
interface IIdentifiable <> interface ILiving <> // интерфейсы IIdentifiable и ILiving вместе образуют логически связанную композицию, // которую можно выделить в тип интерфейс IAnimal interface IAnimal extends IIdentifiable, ILiving <>
Для тех, кто только знакомится понятием интерфейса, будет нелишним узнать о “Принципе разделения интерфейсов” (Interface Segregation Principle или сокращенно ISP), который гласит, что более крупные интерфейсы нужно “дробить” на более мелкие интерфейсы. Но нужно понимать, что условия дробления диктуются конкретным приложением. Если во всех случаях руководствоваться только принципами, то можно раздуть небольшое приложение до масштабов вселенной.
Для примера представьте приложение, которое только выводит в консоль информацию о животных. Так как над объектом Animal будет выполняться только одна операция, то можно не бояться разгневать богов объектно-ориентированного проектирования и включить все нужные характеристики прямо в интерфейс IAnimal .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
interface IAnimal id: string; age: number; > class Animal implements IAnimal public age: number = 0; constructor(readonly id: string) <> > class AnimalUtil public static print(animal: IAnimal): void console.log(animal); > > class Bird extends Animal <> class Raven extends Bird constructor() super('raven'); > > let raven: Raven = new Raven(); AnimalUtil.print(raven);
В такой программе, кроме достоинства архитектора, ничего пострадать не может, так как она выполняет только одну операцию вывода информации о животном.
Но если переписать программу, чтобы она выполняла несколько не связанных логически операций над одним типом, в данном случае IAnimal , то ситуация изменится на противоположную.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
interface IAnimal /*. */ > class Animal implements IAnimal /*. */ > class AnimalUtil public static printId(animal: IAnimal): void console.log(animal.id); // вывод id > public static printAge(animal: IAnimal): void console.log(animal.age); // вывод age > > class Bird extends Animal <> class Raven extends Bird /*. */ > let raven: Raven = new Raven(); AnimalUtil.printId(raven); AnimalUtil.printAge(raven);
В этом случае программа нарушает принцип ISP, так как статические методы printId и printAge получили доступ к данным, которые им не требуются для успешного выполнения. Это может привести к намеренной или случайной порче данных.
1 2 3 4 5 6 7 8 9 10 11 12 13
class AnimalUtil public static printId(animal: IAnimal): void // для успешного выполнения этого метода // не требуется доступ к данным о animal.age console.log(animal.id); > public static printAge(animal: IAnimal): void // для успешного выполнения этого метода // не требуется доступ к данным о animal.id console.log(animal.age); > >
Поэтому в подобных ситуациях настоятельно рекомендуется “дробить” типы интерфейсов на меньшие составляющие и затем ограничивать им доступ к данным.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
interface IIdentifiable <> interface ILiving <> interface IAnimal extends IIdentifiable, ILiving /*. */ > class Animal implements IAnimal /*. */ > class AnimalUtil public static printId(animal: IIdentifiable): void // параметр animal ограничен типом IIdentifiable console.log(animal.id); > public static printAge(animal: ILiving): void // параметр animal ограничен типом ILiving console.log(animal.age); > > class Bird extends Animal <> class Raven extends Bird /*. */ > let raven: Raven = new Raven(); AnimalUtil.printId(raven); AnimalUtil.printAge(raven);
Расширение интерфейсом класса (extends class)¶
В случаях, когда требуется создать интерфейс для уже имеющегося класса, нет необходимости тратить силы на перечисление членов класса в интерфейсе. В TypeScript интерфейсу достаточно расширить тип класса.
Когда интерфейс расширяет класс, он наследует описание членов, но не их реализацию.
1 2 3 4 5 6 7 8 9 10 11 12 13
class Animal nickname: string; age: number; > interface IAnimal extends Animal <> class Bird implements IAnimal nickname: string; age: number; > let bird: IAnimal = new Bird();
Но с расширением класса интерфейсом существует один нюанс.
Интерфейс, полученный путем расширения типа класса, может быть реализован только самим этим классом или его потомками, поскольку помимо публичных ( public ) также наследует закрытые ( private ) и защищенные ( protected ) члены.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class Animal private uid: string; protected maxAge: number; public name: string; > interface IAnimal extends Animal <> class Bird extends Animal implements IAnimal // Ok // private uid: string = ''; // Error, private protected maxAge: number = 100; // Ok, protected public name: string = 'bird'; // Ok, public > class Fish implements IAnimal // Error public name: string = 'fish'; > let bird: IAnimal = new Bird(); // Ok let fish: IAnimal = new Fish(); // Error
Описание класса (функции-конструктора)¶
Известный факт, что в JavaScript, а, следовательно, и в TypeScript, конструкция class — это лишь “синтаксический сахар” над старой доброй функцией-конструктором. Эта особенность позволяет описывать интерфейсы не только для экземпляров класса, но и для самих классов (функций-конструкторов). Проще говоря, с помощью интерфейса можно описать как конструктор, так и статические члены класса, с одной оговоркой — этот интерфейс можно использовать только в качестве типа. То есть класс не может указывать реализацию такого интерфейса с помощью ключевого слова implements , сопряженную с экземпляром, а не самим классом.
Описание интерфейса для функции конструктора может потребоваться, когда в качестве значения выступает сам класс.
Конструктор указывается с помощью ключевого слова new , затем открываются фигурные скобки, в которых при наличии указываются параметры, а в конце указывается тип возвращаемого значения.
new(p1: type, p2: type): type;
Статические члены описываются так же, как и члены экземпляра.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
interface IAnimal nickname: string; > class Animal implements IAnimal nickname: string; constructor(nickname: string) this.nickname = nickname; > > class Bird extends Animal static DEFAULT_NAME: string = 'bird'; static create(): IAnimal return new Bird(Bird.DEFAULT_NAME); > > class Fish extends Animal static DEFAULT_NAME: string = 'bird'; static create(): IAnimal return new Bird(Bird.DEFAULT_NAME); > > const bird: Bird = new Bird('bird'); const fish: Fish = new Fish('fish'); let a: IAnimal[] = [bird, fish]; // Ok, массив экземпляров классов, реализующих интерфейс IAnimal let b: IAnimal[] = [Bird, Fish]; // Error, массив классов interface IAnimalConstructor // декларация интерфейса для класса create(): IAnimal; // static method new (nickname: string): IAnimal; // конструктор > let c: IAnimalConstructor[] = [Bird, Fish]; // Ok, массив классов let d: IAnimal[] = c.map((item) => item.create()); // Ok, массив экземпляров классов, реализующих интерфейс IAnimal
Описание функционального выражения¶
Помимо экземпляров и самих классов, интерфейсы могут описывать функциональные выражения. Это очень удобно, когда функциональный тип имеет большую сигнатуру, которая делает код менее читабельным.
1 2 3 4 5 6 7
// reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T; var callback: ( previusValue: number, currentValue: number, currentIndex: number, array: number[] ) => number;
В большинстве подобных случаев можно прибегнуть к помощи вывода типов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T; var callback: ( previusValue: number, currentValue: number, currentIndex: number, array: number[] ) => number; var callback = ( previusValue: number, currentValue: number, currentIndex: number, array: number[] ) => previusValue + currentValue; let numberAll: number[] = [5, 5, 10, 30]; let sum: number = numberAll.reduce(callback); // 50
Но в случае, если функциональное выражение является параметром функции, как, например, метод массива reduce , то решением может служить только явная декларация типа.
1 2 3 4 5 6 7 8 9 10 11 12 13
class CollectionT> reduce( callbackfn: ( previousValue: T, currentValue: T, currentIndex: number, array: T[] ) => T, initialValue?: T ): T return null; > >
Поэтому при необходимости указать тип явно, помимо рассмотренного в главе Type Queries (запросы типа), Alias (псевдонимы типа) механизма создания псевдонимов типа ( type ), можно описать функциональное выражение с помощью интерфейса.
Для этого необходимо в теле интерфейса описать сигнатуру функции без указания идентификатора.
1 2 3 4 5 6 7 8 9 10
interface ISumAll (. valueAll: number[]): number; > const sumAll: ISumAll = (. valueAll: number[]) => valueAll.reduce((result, value) => (result += value), 0); let numberAll: number[] = [5, 5, 10, 30]; let sum: number = sumAll(. numberAll);
Описание индексных членов в объектных типах¶
Индексные члены подробно будут рассматриваться в главе Объектные типы с индексными членами (объектный тип с динамическими ключами), но не будет лишним и здесь коснуться этого механизма.
1 2 3 4
interface IIndentifier [BindingIdentifier: string]: Type; [BindingIdentifier: number]: Type; >
Инлайн интерфейсы (Inline Interface)¶
Помимо описания объекта, в конструкции, объявляемой с помощью ключевого слова interface , тип объекта можно описать прямо в месте указания типа. Такой способ объявления типа неформально обозначается как инлайн интерфейс (inline interface). Всё ранее описанное для типов интерфейсов, объявленных с помощью ключевого слова interface , в полной мере верно и для их инлайн аналогов.
Различие между ними заключается в том, что второй обладает только телом и объявляется прямо в аннотации типа.
let identifier: p1: type; p2: type >;
Интерфейс, объявленный с помощью ключевого слова interface , считается идентичным инлайн интерфейсу, если их описание совпадает. Но стоит обратить внимание, что это возможно благодаря структурной типизации, которая рассматривается в главе Совместимость типов на основе вида типизации.
1 2 3 4 5 6 7 8 9 10 11 12 13
interface IAnimal nickname: string; > class Bird implements IAnimal nickname: string; > class Fish implements IAnimal nickname: string; > let bird: IAnimal = new Bird(); // Ok let fish: nickname: string > = new Fish(); // Ok
Как было сказано ранее, инлайн интерфейс можно объявлять в тех местах, в которых допускается указание типа. Тем не менее реализовывать ( implements ) и расширять ( extends ) инлайн интерфейс нельзя.
1 2 3 4 5 6 7 8 9 10
interface IT1 <> interface IT2 <> interface IT3 extends f1: IT1, f2: IT2 > // Error > class T4 implements f1: T1, f2: T2 > // Error >
Хотя последнее утверждение и не совсем верно. В дальнейшем будет рассказано о такой замечательной конструкции, как обобщения (глава Обобщения (Generics)), в которых, как раз таки возможно расширять ( extends ) инлайн интерфейсы.
Слияние интерфейсов¶
В случае, если в одной области видимости объявлено несколько одноимённых интерфейсов, то они будут объединены в один.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// так видят разработчики interface IAnimal name: string; > interface IAnimal age: number; > //так видит компилятор /** interface IAnimal name: string; age: number; > */ // разработчики получают то, что видит компилятор let animal: IAnimal; animal.name = 'animal'; // Ok animal.age = 0; // Ok
При попытке переопределить тип поля возникнет ошибка.
1 2 3 4 5 6 7 8 9
interface IAnimal name: string; age: number; > interface IAnimal name: string; // Ok age: string; // Error >
Если в нескольких одноимённых интерфейсах будут описаны одноимённые методы с разными сигнатурами, то они будут расценены, как описание перегрузки. К тому же, интерфейсы, которые описывают множество одноимённых методов, сохраняют свой внутренний порядок.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
interface IBird <> interface IFish <> interface IInsect <> interface IReptile <> // до компиляции interface IAnimalFactory getAnimalByID(id: number): IBird; > interface IAnimalFactory getAnimalByID(id: string): IFish; > interface IAnimalFactory getAnimalByID(id: boolean): IInsect; getAnimalByID(id: object): IReptile; > /** при компиляции interface IAnimalFactory getAnimalByID(id: string): IInsect; getAnimalByID(id: string): IReptile; getAnimalByID(id: string): IFish; getAnimalByID(id: string): IBird; > */ let animal: IAnimalFactory; let v1 = animal.getAnimalByID(0); // Ok -> v1: IBird let v2 = animal.getAnimalByID('5'); // Ok -> v2: IFish let v3 = animal.getAnimalByID(true); // Ok -> v3: IInsect let v4 = animal.getAnimalByID(<>); // Ok -> v4: IReptile
Исключением из этого правила являются сигнатуры, которые имеют в своем описании литеральные строковые типы данных ( literal String Types ). Дело в том, что сигнатуры, содержащие в своем описании литеральные строковые типы, всегда размещаются перед сигнатурами, у которых нет в описании литеральных строковых типов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
interface IBird <> interface IFish <> interface IInsect <> interface IReptile <> // до компиляции interface IAnimalFactory getAnimalByID(id: string): IBird; > interface IAnimalFactory getAnimalByID(id: 'fish'): IFish; > interface IAnimalFactory getAnimalByID(id: 'insect'): IInsect; getAnimalByID(id: number): IReptile; > /** при компиляции interface IAnimalFactory getAnimalByID(id: 'fish'): IFish; getAnimalByID(id: 'insect'): IInsect; getAnimalByID(id: number): IReptile; getAnimalByID(id: string): IBird; > */