TypeScript. Интерфейсы
Интерфейсы TypeScript позволяют описать свой собственный тип данных, перечислив требуемые свойства и методы, дать этому типу название и использовать его в дальнейшем (реализовать этот интерфейс).
Необязательные свойства
Бывает, что не все свойства интерфейса должны обязательно присутствовать у объекта, который этот интерфейс реализует. Это можно описать следующим способом:
Еще таким образом предотвращается возможность использования свойств, не описанных в интерфейсе.
Свойства только на чтение (readonly)
Некоторые свойства объекта должны быть заданы при создании объекта и не могут быть модифицированы в дальнейшем. В таком случае можно воспользоваться конструкцией readonly :
Проверка на наличие лишних свойств
Рассмотрим на первом примере:
TypeScript говорит нам, что свойство size не числится в описании интерфейса ILabelled и является лишним.
Обычно не нужно прибегать к подобным методам обхода проверки, ибо, как правило, если TypeScript указывает на ошибку, то так оно и есть. Может просто необходимо изменить описание интерфейса?
Тип для функций
Использование аналогично обычному интерфейсу:
Названия параметров функции могут не совпадать с названиями параметров в интерфейсе (учитывается порядок их следования). Типы параметров функции можно также не указывать:
Тип для индексируемых свойств
Тип индекса собственно может быть только числовым или строковым.
Реализация интерфейса классом
Классы могут реализовывать интерфейсы следующим образом:
Класс может реализовывать несколько интерфейсов (перечисляются через запятую):
Наследование интерфейсов
В TypeScript есть возможность наследовать один интерфейс от другого. Для этого используется ключевое слово extends (англ. расширять, раздвигать). Это позволяет одному интерфейсу приобретать свойства и методы другого интерфейса, что добавляет некоторой гибкости.
Можно наследоваться от нескольких интерфейсов, перечислив их через запятую:
Наследование от классов
Интерфейс может унаследовать какой-то класс. При этом будут унаследованы все свойства и методы этого класса, включая приватные и защищенные (само собой без реализации). Синтаксис аналогичный:
Но есть заморочка именно с этими приватными и защищенными свойствами класса. Если интерфейс их унаследовал, то реализовать такой интерфейс смогут только сам этот класс, либо его наследники. Это можно использовать для того, чтобы некоторый код работал только с определенными классами, у которых есть определенные методы.
Уффф, объемная тема.
Павел Прудников
Постигающий дзен фулстэк вэб буддизма
Поделиться
Подписаться на Блог MEAN stack разработчика
Получайте свежие записи прямо на ваш почтовый ящик.
Или подпишитесь на RSS с Feedly!
Комментарии
TypeScript. Классы
Наконец-таки наследование здорового человека, через классы, а не через ломающие мозг прототипы! Теперь и в JavaScript! Скоро. Но пока…
TypeScript. Деструктурирующее присваивание
В спецификации ECMAScript 2015 впервые появилось определение «Деструктурирующее присваивание». Когда-то эта фича меня порадовала в Python. Теперь…
Interfaces
Несмотря на то, что тема относящаяся к интерфейсам очень проста, именно она вызывает наибольшее количество вопросов у начинающих разработчиков. Поэтому, такие вопросы как для чего нужны интерфейсы, когда их применять, а когда нет, будут подробно рассмотрены в этой главе.
Общая теория
По факту, интерфейс затрагивает сразу несколько аспектов создания программ, относящихся к проектированию, реализации, конечной сборке. Поэтому, что бы понять предназначение интерфейса, необходимо рассмотреть каждый аспект по отдельности.
Первый аспект — реализация — предлагает рассматривать создаваемые экземпляры как социальные объекты, чья публичная часть инфраструктуры была оговорена в контракте, к коему относится интерфейс. Другими словами, интерфейс — это контракт, реализация которого гарантирует наличие оговоренных в нем членов потребителю экземпляра. Поскольку интерфейс описывает исключительно типы членов объекта (поля, свойства, сигнатуры методов), они не могут гарантировать, что сопряженная с ними логика будет соответствовать каким-либо критериям. По этому случаю была принята методология называемая контрактное программирование. Несмотря на то, что данная методология вызывает непонимание у большинства начинающих разработчиков, в действительности она очень проста. За этим таинственным термином скрываются рекомендации придерживаться устной или письменной спецификации при реализации логики, сопряженной с оговоренными в интерфейсе членами.
Второй аспект — проектирование — предлагает проектировать объекты менее независимыми за счет отказа от конкретных типов (классов) в пользу интерфейсов. Ведь пока тип переменной или параметра представляется классовым типом, невозможно будет присвоить значение соответствующее этому типу, но не совместимое с ним. Под соответствующим подразумевается соответствие по всем обязательным признакам, но не состоящим в отношениях наследования. И хотя в TypeScript из-за реализации номинативной типизации подобной проблемы не существует, по возможности рекомендуется придерживаться классических взглядов.
Третий аспект — сборка — вытекает из второго и предполагает уменьшение размера компилируемого пакета (bundle) за счет отказа от конкретных типов (классов). Фактически, если какой-либо объект требуется пакету лишь для выполнения операций над ним, последнему вовсе не нужно содержать определение первого. Другими словами, скомпилированный пакет не должен включать определение класса со всей его логикой только потому, что он указан в качестве типа. Для этого как нельзя лучше подходят типы, представленные интерфейсами. Хотя нельзя не упомянуть, что данная проблема не имеет никакого практического отношения к разработчикам на языке TypeScript, поскольку его (или точнее сказать JavaScript) модульная система лишена подобного недостатка.
Вот эти несколько строк, описывающие оговоренные в самом начале аспекты, заключают в себе ответы на все возможные вопросы, которые только могут возникнуть относительно темы сопряженной с интерфейсами. Если ещё более доступно, то интерфейсы нужны для снижения зависимости и наложения обязательств на реализующие их классы. Интерфейсы стоит применять всегда и везде, где это возможно. Это не только повысит семантическую привлекательность кода, но и сделает его более поддерживаемым.
Не лишним будет добавить, что интерфейсы являются фундаментальной составляющей идеологии как типизированных языков, так и объектно-ориентированного программирования.
Такая известная группа программистов, как “Банда четырех” (Gang of Four, сокращённо GoF), в своей книге, положившей начало популяризации шаблонов проектирования, описывали интерфейс как ключевую концепцию объектно-ориентированного программирования (ооп). Понятие интерфейса является настолько важным, что в книге был сформулирован принцип объектно-ориентированного проектирования, который звучит так: Программируйте в соответствии с интерфейсом, а не с реализацией.
Другими словами, авторы советуют создавать систему, которой вообще ничего не будет известно о реализации. Проще говоря, создаваемая система должна быть построена на типах, определяемых интерфейсами, а не на типах, определяемых классами.
С теорией закончено. Осталось подробно рассмотреть реализацию интерфейсов в TypeScript.
Интерфейс в TypeScript
TypeScript предлагает новый тип данных, определяемый с помощью синтаксической конструкции называемой интерфейс ( interface ).
Interface — это синтаксическая конструкция, предназначенная для описания открытой ( public ) части объекта без реализации (api). Хотя не будет лишним упомянуть, что существуют языки позволяющие реализовывать в интерфейсах поведение, рассматриваемое как поведение по умолчанию.
Объявление (declaration)
Объявление интерфейса возможно как в контексте модуля, так и в контексте функции или метода.
Конвенции именования интерфейсов
Прежде чем продолжить, нужно обратить внимание на такой аспект, как конвенции именования интерфейсов. Существует два вида именования.
Первый вид конвенций родом из языка Java — они предлагают именовать интерфейсы точно так же как и классы. Допускаются имена прилагательные.
Чтобы сразу расставить все точки над i, стоит заметить, что в дальнейшем идентификаторы интерфейсов будут указываться по конвенциям C#.
Реализация интерфейса (implements)
Один класс может реализовывать сколько угодно интерфейсов. В этом случае реализуемые интерфейсы должны быть перечислены через запятую.
В случае, когда класс расширяет другой класс, указание реализации ( implements ) следует после указания расширения ( extends ).
Декларация свойств get и set (accessors)
Несмотря на то, что в интерфейсе можно декларировать поля и методы, в нем нельзя декларировать свойства get и set (аксессоры). Но, несмотря на это, задекларированное в интерфейсе поле может быть совместимо не только с полем, но и аксессорами. При этом нет разницы, будет в объекте объявлен getter, setter или оба одновременно.
Указание интерфейса в качестве типа (interface types)
Класс, реализующий интерфейс, принадлежит к типу этого интерфейса. Класс, унаследованный от класса реализующего интерфейс, также наследует принадлежность к реализуемым им интерфейсам. В подобных сценариях говорят, что класс наследует интерфейс.
Расширение интерфейсов (extends interface)
Для тех, кто только знакомится с понятием интерфейса, будет не лишним узнать о “Принципе разделения интерфейсов” (Interface Segregation Principle или сокращенно ISP), который гласит, что более крупные интерфейсы нужно “дробить” на более мелкие интерфейсы. Но нужно понимать, что условия дробления диктуются конкретным приложением. Если во всех случаях руководствоваться только принципами, то можно раздуть небольшое приложение до масштабов вселенной.
В такой программе, кроме достоинства архитектора, ничего пострадать не может, так как она выполняет только одну операцию вывода информации о животном.
В этом случае программа нарушает принцип ISP, так как статические методы printId и printAge получили доступ к данным, которые им не требуются для успешного выполнения. Это может привести к намеренной или случайной порче данных.
Поэтому в подобных ситуациях настоятельно рекомендуется “дробить” типы интерфейсов на меньшие составляющие и затем ограничивать ими доступ к данным.
Расширение интерфейсом класса (extends class)
В случаях, когда требуется создать интерфейс для уже имеющегося класса, нет необходимости тратить силы на перечисление членов класса в интерфейсе. В TypeScript интерфейсу достаточно расширить тип класса.
Когда интерфейс расширяет класс, он наследует описание членов, но не их реализацию.
Но с расширением класса интерфейсом существует один нюанс.
Интерфейс, полученный путем расширения типа класса, может быть реализован только самим этим классом или его потомками, поскольку помимо публичных ( public ) также наследует закрытые ( private ) и защищенные ( protected ) члены.
Описание класса (функции-конструктора)
Известный факт, что в JavaScript, а следовательно и в TypeScript, конструкция class — это лишь “синтаксический сахар” над старой доброй функцией-конструктором. Эта особенность позволяет описывать интерфейсы не только для экземпляров класса, но и для самих классов (функций-конструкторов). Проще говоря, с помощью интерфейса можно описать как конструктор, так и статические члены класса, с одной оговоркой — этот интерфейс можно использовать только в качестве типа. То есть класс не может указывать реализацию такого интерфейса с помощью ключевого слова implements сопряженную с экземпляром, а не самим классом.
Описание интерфейса для функции конструктора может потребоваться когда в качестве значения выступает сам класс.
Статические члены описываются так же, как и члены экземпляра.
Описание функционального выражения
Помимо экземпляров и самих классов, интерфейсы могут описывать функциональные выражения. Это очень удобно, когда функциональный тип имеет большую сигнатуру, которая делает код менее читабельным.
В большинстве подобных случаев можно прибегнуть к помощи вывода типов.
Для этого необходимо в теле интерфейса описать сигнатуру функции без указания идентификатора.
Описание индексных членов в объектных типов
Инлайн интерфейсы (Inline Interface)
Различие между ними заключается в том, что второй обладает только телом и объявляется прямо в аннотации типа.
Как было сказано ранее, инлайн интерфейс можно объявлять в тех местах, в которых допускается указание типа. Тем не менее, реализовывать ( implements ) и расширять ( extends ) инлайн интерфейс нельзя.
Слияние интерфейсов
В случае, если в одной области видимости объявлено несколько одноимённых интерфейсов, то они будут объединены в один.
При попытке переопределить тип поля, возникнет ошибка.
Если в нескольких одноимённых интерфейсах будут описаны одноимённые методы с разными сигнатурами, то они будут расценены, как описание перегрузки. К тому же, интерфейсы, которые описывают множество одноимённых методов, сохраняют свой внутренний порядок.
Исключением из этого правила являются сигнатуры, которые имеют в своем описании литеральные строковые типы данных ( literal String Types ). Дело в том, что сигнатуры содержащие в своем описании литеральные строковые типы, всегда размещаются перед сигнатурами, у которых нет в описании литеральных строковых типов.
Interfaces¶
Несмотря на то, что тема относящаяся к интерфесам очень проста, именно она вызывает наибольшее количество вопросов у начинающих разработчиков. Поэтому такие вопросы как для чего нужны интерфейсы, когда их применять, а когда нет, будет подробно рассмотрены в этой главе.
Общая теория¶
По факту интерфейс затрагивает сразу несколько аспектов создания программ относящихся к проектированию, реализации, конечной сборке. Поэтому, чтобы понять предназначение интерфейса, необходимо рассмотреть каждый аспект по отдельности.
Первый аспект (реализация) предлагает рассматривать создаваемые экземпляры как социальные объекты чья публичная часть инфраструктуры была оговорена в контракте, к коему относится интерфейс. Другими словами интерфейс это контракт реализация которого гарантирует наличие оговоренных в нем членов потребителю экземпляра. Поскольку интерфейс описывает исключительно типы членов объекта (поля, свойства, сигнатуры методов) они не могут гарантировать что сопряженная с ними логика будет соответствовать каким-либо критериям. Поэтому случаю была принята методология называемая контрактное программирование. Несмотря на то что данная методология вызывает непонимание у большинства начинающих разработчиков, в действительности она очень проста. За этим таинственным термином скрываются рекомендации придерживаться устной или письменной спецификации при реализации логики сопряженной с оговоренными в интерфейсе членами.
Второй аспект (проектирование) предлагает проектировать объекты менее независимыми за счет отказа от конкретных типов (классов) в пользу интерфейсов. Ведь пока тип переменной или параметра представляется классовым типом, невозможно будет присвоить значение соответствующее этому типу, но не совместимое с ним. Под соответствующим подразумевается соответствие по всем обязательным признакам, но не состоящим в отношениях наследования. И хотя в TypeScript из-за реализации номинативной типизации подобной проблемы не существует, по возможности рекомендуется придерживаться классических взглядов.
Третий аспект (сборка) вытекает из второго и предполагает уменьшение размера компилируемого пакета (bundle) за счет отказа от конкретных типов (классов). Фактически если какой-либо объект требуется пакету лишь для выполнения операций над ним, последнему вовсе не нужно содержать определение первого. Другими словами скомпелированный пакет не должен включать определение класса со всей его логикой только потому, что он указан в качестве типа. Для этого как нельзя лучше подходят типы представленные интерфейсами. Хотя нельзя не упомянуть, что данная проблема не имеет никакого практического отношения к разработчикам на языке TypeScript поскольку его (или точнее сказать JavaScript) модульная система лишена подобного недостатка.
Вот эти несколько строк описывающие оговоренные в самом начале аспекты заключают в себе ответы на все возможные вопросы которые только могут возникнуть относительно темы сопряженной с интерфейсами. Если ещё более доступно, то интерфейсы нужны для снижения зависимости и наложения обязательств на реализующие их классы. Интерфейсы стоит применять всегда и везде, где это возможно. Это не только повысит семантическую привлекательность кода, но и сделает его более поддерживаемым.
Не лишним будет добавить что интерфейсы являются фундаментальной составляющей идеологии как типизированных языков, так и объектно-ориентированного программирования.
Такая известная группа программистов, как “Банда четырех” (Gang of Four, сокращённо GoF), в своей книге, положившей начало популяризации шаблонов проектирования, описывали интерфейс как ключевую концепцию объектно-ориентированного программирования (ооп). Понятие интерфейса является настолько важным, что в книге был сформулирован принцип объектно-ориентированного проектирования, который звучит так: Программируйте в соответствии с интерфейсом, а не с реализацией.
Другими словами, авторы советуют создавать систему, которой вообще ничего не будет известно о реализации. Проще говоря, создаваемая система должна быть построена на типах, определяемых интерфейсами, а не на типах, определяемых классами.
С теорией закончено. Осталось подробно рассмотреть реализацию интерфейсов в TypeScript.
Интерфейс в TypeScript¶
TypeScript предлагает новый тип данных, определяемый с помощью синтаксической конструкции называемой интерфейс ( interface ).
Interface — это синтаксическая конструкция предназначенная для описания открытой ( public ) части объекта без реализации (api). Хотя не будет лишним упомянуть, что существуют языки позволяющие реализовывать в интерфейсах поведение рассматриваемое как поведение по умолчанию.
Объявление (declaration)¶
В TypeScript интерфейс объявляется с помощью ключевого слова interface после которого указывается идентификатор (имя) за которым следует тело заключенное в фигурные скобки содержащее описание.
Объявление интерфейса возможно как в контексте модуля, так и в контексте функции или метода.
Конвенции именования интерфейсов¶
Прежде чем продолжить, нужно обратить внимание на такой аспект, как конвенции именования интерфейсов. Существует два вида именования.
Первый вид конвенций родом из языка Java — они предлагают именовать интерфейсы точно так же как и классы. Допускаются имена прилагательные.
Чтобы сразу расставить все точки над i, стоит заметить, что в дальнейшем идентификаторы интерфейсов будут указываться по конвенциям C#.
Реализация интерфейса (implements)¶
Один класс может реализовывать сколько угодно интерфейсов. В этом случае реализуемые интерфейсы должны быть перечислены через запятую.
В случае, когда класс расширяет другой класс, указание реализации ( implements ) следует после указания расширения ( extends ).
Декларация свойств get и set (accessors)¶
Несмотря на то, что в интерфейсе можно декларировать поля и методы, в нем нельзя декларировать свойства get и set (аксессоры). Но, несмотря на это, задекларированное в интерфейсе поле может быть совместимо не только с полем, но и аксессорами. При этом нет разницы, будет в объекте объявлен getter, setter или оба одновременно.
Указание интерфейса в качестве типа (interface types)¶
Класс реализующий интерфейс принадлежит к типу этого интерфейса. Класс унаследованный от класса реализующего интерфейс, также наследует принадлежность к реализуемым им интерфейсам. В подобных сценариях говорят что класс наследует интерфейс.
Класс, реализующий множество интерфейсов принадлежит к типу каждого из них. Когда экземпляр класса реализующего интерфейс присваивают ссылке с типом интерфейса, то говорят что экземпляр был ограничен типом интерфейса. То есть, функционал экземпляра класса урезается до описанного в интерфейсе (подробнее об этом речь пойдет в главе Совместимость объектов) и Совместимость функций).
Несмотря на то, что интерфейс является синтаксической конструкцией и может указываться в качестве типа, после компиляции от него не остается и следа. Это в свою очередь означает, что интерфейс, как тип данных, может использоваться только на этапе компиляции. Другими словами, компилятор сможет предупредить об ошибках несоответствия объекта описанному интерфейсу, но проверить на принадлежность к типу интерфейса с помощью операторов typeof или instanceof не получится поскольку они выполняются во время выполнения программы. Но в TypeScript существует механизм (который будет рассмотрен далее в главе Защитники типа), позволяющий в некоторой мере решить эту проблему.
Расширение интерфейсов (extends interface)¶
Для тех кто только знакомится понятием интерфейса, будет не лишним узнать о “Принципе разделения интерфейсов” (Interface Segregation Principle или сокращенно ISP), который гласит, что более крупные интерфейсы нужно “дробить” на более мелкие интерфейсы. Но нужно понимать, что условия дробления диктуются конкретным приложением. Если во всех случаях руководствоваться только принципами, то можно раздуть небольшое приложение до масштабов вселенной.
В такой программе, кроме достоинства архитектора, ничего пострадать не может, так как она выполняет только одну операцию вывода информации о животном.
В этом случае программа нарушает принцип ISP, так как статические методы printId и printAge получили доступ к данным, которые им не требуются для успешного выполнения. Это может привести к намеренной или случайной порче данных.
Поэтому в подобных ситуациях настоятельно рекомендуется “дробить” типы интерфейсов на меньшие составляющие и затем ограничивать ими доступ к данным.
Расширение интерфейсом класса (extends class)¶
В случаях, когда требуется создать интерфейс для уже имеющегося класса, нет необходимости тратить силы на перечисление членов класса в интерфейсе. В TypeScript интерфейсу достаточно расширить тип класса.
Когда интерфейс расширяет класс, он наследует описание членов, но не их реализацию.
Но с расширением класса интерфейсом существует один нюанс.
Интерфейс полученный путем расширения типа класса может быть реализован только самим этим классом или его потомками, поскольку помимо публичных ( public ) также наследует закрытые ( private ) и защищенные ( protected ) члены.
Описание класса (функции-конструктора)¶
Описание интерфейса для функции конструктора может потребоваться когда в качестве значения выступает сам класс.
Статические члены описываются также, как и члены экземпляра.
Описание функционального выражения¶
Помимо экземпляров и самих классов, интерфейсы могут описывать функциональные выражения. Это очень удобно, когда функциональный тип имеет большую сигнатуру, которая делает код менее читабельным.
В большинство подобных случаев можно прибегнуть к помощи вывода типов.
Поэтому при необходимости указать тип явно, помимо рассмотренного в главе Type Queries (запросы типа), Alias (псевдонимы типа) механизма создания псевдонимов типа ( type ), можно описать функциональное выражение с помощью интерфейса.
Для этого необходимо в теле интерфейса описать сигнатуру функции без указания идентификатора.
Описание индексных членов в объектных типов¶
Индексные члены подробно будут рассматриваться в главе Объектные типы с индексными членами (объектный тип с динамическими ключами), но не будет лишним и здесь коснутся этого механизма.
Инлайн интерфейсы (Inline Interface)¶
Различие между ними заключается в том, что второй обладает только телом и объявляется прямо в аннотации типа.
Как было сказано ранее, инлайн интерфейс можно объявлять в тех местах, в которых допускается указание типа. Тем не менее реализовывать ( implements ) и расширять ( extends ) инлайн интерфейс нельзя.
Хотя последнее утверждение и не совсем верно. В дальнейшем будет рассказано о такой замечательной конструкции, как обобщения (глава Обобщения (Generics)), в которых, как раз таки возможно расширять ( extends ) инлайн интерфейсы.
Слияние интерфейсов¶
В случае, если в одной области видимости объявлено несколько одноимённых интерфейсов, то они будут объединены в один.
При попытке переопределить тип поля, возникнет ошибка.
Если в нескольких одноимённых интерфейсах будут описаны одноимённые методы с разными сигнатурами, то они будут расценены, как описание перегрузки. К тому же, интерфейсы, которые описывают множество одноимённых методов, сохраняют свой внутренний порядок.
Исключением из этого правила являются сигнатуры, которые имеют в своем описании литеральные строковые типы данных ( literal String Types ). Дело в том, что сигнатуры содержащие в своем описании литеральные строковые типы, всегда размещаются перед сигнатурами, у которых нет в описании литеральных строковых типов.



