Table of Contents #
Важной частью разработки является построение компонентов, которые обладали бы не только строгим и последовательным API, но и были доступны для повторного использования. Наиболее гибкие возможности для построения крупной программной системы дают компоненты, способные работать и с теми данными, которые есть сегодня, и с теми, которые могут появиться завтра.
В таких языках, как C# и Java, одним из главных инструментов для создания компонентов, предназначенных для повторного использования, являются обобщения (также известные как шаблоны или дженерики). Они позволяют создавать компоненты, способные работать с различными типами, а не только с каким-то одним. Это позволяет применять такие компоненты и использовать свои типы.
Здравствуй, мир обобщений! #
Без использования обобщений пришлось бы задать такой функции определенный тип:
Или же описать ее, используя тип any :
Вместо этого нужен способ захватить тип аргумента так, чтобы его впоследствии можно было использовать для описания типа возвращаемого значения. Здесь мы используем ти́повую переменную — особый вид переменной, которая оперирует типами, а не значениями.
Мы добавили типовую переменную T к функции-тождеству. Эта T позволяет сохранять тип, который указал пользователь (то есть number ), так что позже его можно будет использовать. В данном случае T используется в качестве типа возвращаемого значения. Можно увидеть, что теперь и аргумент, и возвращаемое значение имеют один и тот же тип. Такой способ позволяет направить информацию о типах со входа функции к ее выходу.
Функция написана, и теперь ее можно вызвать одним из двух способов. Первый способ — передать все аргументы, в том числе и типовый аргумент:
Второй способ, вероятно, наиболее популярен. Здесь используется выведение типового аргумента, и компилятор автоматически устанавливает T на основании типа аргумента, который передается в функцию:
Обратите внимание, что тип не передается явно, в угловых скобках ( <> ) — компилятор просто проанализировал значение «myString» и установил T в значение его типа. Хотя выведение типового аргумента может быть полезно, чтобы сделать код более кратким и читаемым, иногда может понадобиться явно передавать типовый аргумент, если компилятору не удается автоматически вывести тип, что может произойти в более сложных случаях.
Работа с обобщенными типовыми переменными #
Возьмем уже знакомую нам функцию identity :
Что, если нужно при каждом вызове функции выводить длину аргумента arg в консоль? Может появиться искушение написать так:
Как вариант, можно записать этот пример следующим способом:
Обобщенные типы #
В предыдущих разделах мы создали обобщенную функцию-тождество, которая работала с различными типами. В этом разделе разберем, как описать тип такой функции и то, как создавать обобщенные интерфейсы.
Тип обобщенной функции схож с типом обычной функции, где типовый параметр указан первым, так же, как и в ее определении:
Для типового параметра можно было бы использовать другое имя, важно лишь, чтобы число типовых параметров и то, как они используются, согласовывалось.
Также можно записать обобщенный тип как сигнатуру вызова на типе объектного литерала:
Это подводит нас к описанию первого обобщенного интерфейса. Возьмем объектный литерал из предыдущего примера и превратим его в интерфейс:
Обратите внимание, что пример трансформировался в нечто совершенно иное. Вместо описания обобщенной функции теперь обычная, не обобщенная функция, которая является частью обобщенного типа. При использовании GenericIdentityFn теперь придется указывать соответствующий типовый аргумент (в данном случае number ), таким образом зафиксировав типы, которые будет использовать соответствующая функция. Понимать, в каких случаях типовый параметр нужно добавлять к сигнатуре вызова, а когда — к самому интерфейсу, полезно при описании того, какие аспекты типа являются обобщенными.
Кроме обобщенных интерфейсов можно создавать и обобщенные классы. Обратите внимание, что создавать обобщенные перечисления и пространства имен нельзя.
Обобщенные классы #
Обобщенные классы имеют такой же вид, что и обобщенные интерфейсы. У них есть список типовых параметров в угловых скобках ( <> ) после имени класса.
Так же, как и с интерфейсами, передача типового параметра самому классу устанавливает то, что все его свойства будет работать с одним и тем же типом.
Как указывалось в главе о классах, у класса есть два типа: тип статической части и тип экземпляра. Обобщенные типы являются таковыми только по отношению к типу экземпляра, но не к типу статической части. Поэтому статические члены класса не могут использовать типовые параметры класса.
Ограничения обобщений #
Поскольку обобщенная функция теперь имеет ограничение, она не сможет работать с любым типом:
Вместо этого ей необходимо передавать значения тех типов, у которых есть все необходимые свойства:
Использование типовых параметров в ограничениях обобщений
Можно объявить типовый параметр, который ограничивается другим типовым параметром. К примеру, нужно принять два объекта и копировать свойства из одного в другой. Нужно удостовериться, что мы случайно не добавим какое-либо лишнее свойство, поэтому добавим ограничение между двумя типами:
Использование типов классов в обобщениях
Реализуя паттерн «фабрика» с использованием обобщений, необходимо указывать на тип класса с помощью его функции-конструктора. К примеру,
Более сложный пример использует свойство прототипа, чтобы вывести и ограничить отношения между конструктором и типом экземпляра класса.
TypeScript: Обобщённые типы (Generics)
Серия
В двух предыдущих статьях мы говорили о том, что из себя представляет TypeScript, зачем и когда он может вам понадобиться, а также о том, какие типы и структуры будут встречаться вам каждый день при написании кода на этом надмножестве языка JavaScript.
Также, мы вскользь задели тему обобщений, когда разговаривали о типе пересечения. Сегодня мы поговорим об обобщённых типах подробнее.
Первый взгляд на обобщения
Обобщённый тип (обобщение, дженерик) позволяет резервировать место для типа, который будет заменён на конкретный, переданный пользователем, при вызове функции или метода, а также при работе с классами.
Посмотрим на простейшую реализацию функции, описанной выше, если бы в TypeScript не было обобщённых типов и нам пришлось бы довольствоваться тем, что уже есть.
Всё как в описании: функция принимает аргумент любого типа и возвращает его же. Однако, это не совсем так. Внимательно присмотритесь к типам: в примере указано, что функция принимает аргумент какого-то типа и возвращает значение какого-то типа, но при этом они никак не связаны. Грубо говоря, сейчас мы можем передать аргумент типа number и получить значение типа string – это валидно, потому что any подразумевает под собой всё что угодно.
Сейчас функция представляет собой «чёрный ящик», в который с одной стороны что-то входит, а с другой стороны что-то выходит, возможно, похожее на то, что входило или нет – непонятно. Я бы сказал, что такую функцию можно называть «обезличивающей функцией» в контексте типов. При использовании any в качестве типа возвращаемого значения мы обезличиваем результат функции.
Теперь перепишем функцию так, чтобы каждый из нас знал, что тип переданного аргумента является и типом возвращаемого значения.
В мире C#, технически, можно утверждать, что identity – это открытый тип, а identity – замкнутый. При этом нужно понимать, что вы можете работать только с замкнутыми типами.
Попробуем вызвать эту функцию с аргументом типа number и поверхностно проследим за тем, как компилятор определяет, что вернёт функция.
Логический вывод обобщённых типов
Многих разработчиков смущает указание типа с помощью знаков «меньше» и «больше». Для упрощения создания, чтения и работы с кодом TypeScript имеет возможность логического вывода типов при вызове обобщённых типов. Это значит, что компилятор будет пытаться определить (логически вывести) тип, который автоматически будет использоваться при вызове обобщённого типа.
В отличие от C#, в TypeScript нет возможности написать две функции с одним именем, когда первая функция реализована для конкретного типа, а вторая – для обобщённого. Например, такое в TypeScript написать не получится:
Также стоит упомянуть, что компилятор выдаст ошибку, если он не сможет точно вывести тип для параметр-типа. Для того, чтобы разрешить сложившуюся ситуацию нужно явно указать типы.
Для чего существуют обобщённые типы
При написании кода вы всегда встаёте перед выбором, имеющим как минимум две ветви решения. Первый путь приводит вас к копированию существующего кода с нужным вам типом, что также приводит к «распуханию» кода и необходимости думать при рефакторинге. Второй путь – унификация, что подразумевает под собой параметрический полиморфизм, в основе которого лежит использование одного и того же кода, но с разными типами. Получается, что обобщённые типы нужны для написания кода, который можно многократно использовать вне зависимости от типов.
Безопасность типов
Когда обобщённый код (например, алгоритм) применяется с конкретным типом, компилятор гарантирует нам, что в алгоритме будут использоваться лишь те «объекты», что совместимы с этим типом данных. В случае, если попытаться передать не совместимый с кодом тип, то компилятор породит ошибку.
Более простой и понятный код
Поскольку компилятор защищает нас от передачи аргументов неправильных типов, это позволяет использовать меньше защитников типов, которые были рассмотрены в предыдущей статье. Однако, работает только в том случае, если ваш стек полностью построен на TypeScript и это не будет работать в том случае, если вы распространяете свой код в скомпилированном виде. В таком случае вы обязаны использовать защитники типов, если хотите оберегать своих пользователей от неправильного использования вашего кода.
Базовые ограничения обобщённых типов
Посмотрим на код ниже и попытаемся ответить на вопрос: что здесь может пойти не так?
Ограничения позволяют сузить перечень типов, которые можно будет передать в обобщённом аргументе, и расширяет возможности по работе с этими типами.
Пусть у нас есть три объекта, каждый из которых представлен своим интерфейсом, соответственно:
Теперь попробуем передать в нашу функцию объект из переменной a и b :
Уточнения с помощью обобщений
Все мы знаем, что типов и поколений покемонов так много, что без TypeScript-а и структур данных здесь разобраться не получится. Давайте напишем простейший интерфейс, описывающий покемонов: тип, вес, высота и сила атаки.
А теперь напишем функцию, которая умеет работать не только с интерфейсом покемонов, но и с другими интерфейсами, используя обобщения.
Врядли кто-то из нас будет писать функцию, которая просто возвращает значение свойств без каких-либо ещё действий и логики. Конструкция keyof – это гарант того, что вы пытаетесь передать в функцию только разрешённый объект и существующее имя свойства, при этом взамен вы получаете не только значение свойства, но и его тип.
Ошибок нет. Типов нет. Уверенности в том, что функция не просто работает, а работает правильно – нет. Именно для этого нужна статическая типизация – для того, чтобы быть уверенным в том, что ваша функция работает так, как вы и задумывали. Здесь лишь одна оговорочка: речь идёт только про возвращаемый тип, а не про поведение функции.
Обобщения и интерфейсы
Обобщения не обошли стороной и интерфейсы.
Но это скучно и понятно – давайте попробуем посмотреть на более сложный пример. Допустим, имеется интефейс, описывающий пользователя какого-нибудь простенького сервиса:
Однако не спешите писать такой тип в ваших проектах сами: тип Partial уже существует в TypeScript и не требует пользовательского определения.
Некоторые встроенные типы для обобщений
В функциональном программировании, да и в программировании вообще, часто встречается необходимость изъять из какого-либо объекта не значение одного свойства, а двух или нескольких. Например, это может быть «фильтр» данных. Напишем такой «фильтр», используя уже знакомые нам типы и структуры, в рамках всё тех же покемонов.
Код немного грязный, но я не смог придумать более элегантного решения с типами.
Основные типы для обобщений:
TypeScript имеет большое количество стандартных типов, которые могут использоваться в вашем коде с обобщениями. Полный список не описывается в документации, но вы можете почитать описание ES5. Также существует модуль typescript-collections, имплементирующий многие «стандартные» структуры данных на TypeScript.
Обобщения в классах
Здесь особо не о чем говорить, потому что всё то, что можно сделать с обобщениями в интерфейсах – можно сделать и в классах. Просто имейте в виду. Зато здесь можно поговорить об ограничениях.
Ссылочки
По традиции оставляю пару ссылочек, которые помогут понять материал не только с моей стороны, но и со стороны других авторов. Не удивляйтесь, что в ссылках присутствуют материалы по C#, так как TypeScript берёт многие возможности именно из него – напомню, что у них один автор.
Делимся на оплату хостинга или кофе.
Чем чаще пью кофе, тем чаще пишу статьи.
Манипуляции с типами
Система типов TS позволяет создавать типы на основе других типов.
Простейшей формой таких типов являются дженерики или общие типы (generics). В нашем распоряжении также имеется целый набор операторов типа. Более того, мы можем выражать типы в терминах имеющихся у нас значений.
Дженерики#
Для того, чтобы сделать эту функцию более универсальной, можно использовать тип any :
Однако, при таком подходе мы не будем знать тип возвращаемого функцией значения.
Нам нужен какой-то способ перехватывать тип аргумента для обозначения с его помощью типа возвращаемого значения. Для этого мы можем воспользоваться переменной типа, специальным видом переменных, которые работают с типами, а не со значениями:
Мы используем переменную Type как для типа передаваемого функции аргумента, так и для типа возвращаемого функцией значения.
Такие функции называют общими (дженериками), поскольку они могут работать с любыми типами.
Мы можем вызывать такие функции двумя способами. Первый способ заключается в передаче всех аргументов, включая аргумент типа:
В данном случае принимаемым и возвращаемым типами является строка.
Второй способ заключается в делегировании типизации компилятору:
Второй способ является более распространенным. Однако, в более сложных случаях может потребоваться явное указание типа, как в первом примере.
Работа с переменными типа в дженериках#
Что если мы захотим выводить в консоль длину аргумента arg перед его возвращением?
Изменим сигнатуру функции таким образом, чтобы она работала с массивом Type :
Мы можем сделать тоже самое с помощью такого синтаксиса:
Общие типы#
Тип общей функции (функции-дженерика) похож на тип обычной функции, в начале которого указывается тип параметра:
Мы можем использовать другое название для параметра общего типа:
Мы также можем создавать общие типы в виде сигнатуры вызова типа объектного литерала:
Это приводит нас к общему интерфейсу:
Для того, чтобы сделать общий параметр видимым для всех членов интерфейса, его необходимо указать после названия интерфейса:
Кроме общих интерфейсов, мы можем создавать общие классы.
Общие перечисления (enums) и пространства имен (namespaces) создавать нельзя.
Общие классы#
Общий класс имеет такую же форму, что и общий интерфейс:
В случае с данным классом мы не ограничены числами. Мы вполне можем использовать строки или сложные объекты:
Класс имеет две стороны с точки зрения типов: статическую сторону и сторону экземпляров. Общие классы являются общими только для экземпляров. Это означает, что статические члены класса не могут использовать тип параметра класса.
Ограничения дженериков#
Нам необходимо создать интерфейс, описывающий ограничение. В следующем примере мы создаем интерфейс с единственным свойством length и используем его с помощью ключевого слова extends для применения органичения:
Поскольку дженерик был ограничен, он больше не может работать с любым типом:
Мы должны передавать ему значения, отвечающие всем установленным требованиям:
Использование типов параметров в ограничениях дженериков#
Мы можем определять типы параметров, ограниченные другими типами параметров. В следующем примере мы хотим получать свойства объекта по их названиям. При этом, мы хотим быть уверенными в том, что не извлекаем несуществующих свойств. Поэтому мы помещаем ограничение между двумя типами:
Использование типов класса в дженериках#
При создании фабричных функций с помощью дженериков, необходимо ссылаться на типы классов через их функции-конструкторы. Например:
В более сложных случаях может потребоваться использование свойства prototype для вывода и ограничения отношений между функцией-конструктором и стороной экземляров типа класса:
Данный подход часто используется в миксинах или примесях.
Оператор типа keyof #
Оператор keyof «берет» объектный тип и возвращает строковое или числовое литеральное объединение его ключей:
Типы keyof являются особенно полезными в сочетании со связанными типами (mapped types), которые мы рассмотрим позже.
Оператор типа typeof #
В TS оператор typeof используется в контексте типа для ссылки на тип переменной или свойства:
Ограничения#
typeof можно использовать только в отношении идентификаторов (названий переменных) или их свойств. Это помогает избежать написания кода, который не выполняется:
Типы доступа по индексу (indexed access types)#
Мы можем использовать тип доступа по индексу для определения другого типа:
При попытке доступа к несуществующему свойству возникает ошибка:
Другой способ индексации заключается в использовании number для получения типов элементов массива. Мы также можем использовать typeof для перехвата типа элемента:
Однако, в данном случае мы можем использовать синоним типа (type alias):
Условные типы (conditional types)#
Обычно, в программе нам приходится принимать решения на основе некоторых входных данных. В TS решения также зависят от типов передаваемых аргументов. Условные типы помогают описывать отношения между типами входящих и выходящих данных.
В приведенном примере польза условных типов не слишком очевидна. Она становится более явной при совместном использовании условных типов и дженериков (общих типов).
Рассмотрим такую функцию:
Перегрузки createLabel описывают одну и ту же функцию, которая делает выбор на основе типов входных данных.
Вместо этого, мы можем реализовать такую же логику с помощью условных типов:
Затем мы можем использовать данный тип для избавления от перегрузок:
Ограничения условных типов#
Часто проверка в условном типе дает нам некоторую новую информацию. Подобно тому, как сужение с помощью защитников или предохранителей типа (type guards) возвращает более конкретный тип, инстинная ветка условного типа ограничивает дженерики по типу, который мы проверяем.
Рассмотрим такой пример:
Предположения в условных типах#
Мы использовали условные типы для применения ограничений и извлечения типов. Это является настолько распространенной операцией, что существует особая разновидность условных типов.
В данном случае мы использовали ключевое слово infer для декларативного создания нового дженерика Item вместо извлечения типа элемента T в истинной ветке. Это избавляет нас от необходимости «копаться» и изучать структуру типов, которые нам необходимы.
При предположении на основе типа с помощью нескольких сигнатур вызова (такого как тип перегруженной функции), предположение выполняется на основе последней сигнатуры. Невозможно произвести разрешение перегрузки на основе списка типов аргументов.
Распределенные условные типы (distributive conditional types)#
Когда условные типы применяются к дженерикам, они становятся распределенными при получении объединения (union). Рассмотрим следующий пример:
Здесь StrOrNumArray распределяется на:
и применяется к каждому члену объединения:
что приводит к следующему:
Обычно, такое поведение является ожидаемым. Для его изменения можно обернуть каждую сторону extends в квадратные скобки:
Связанные типы (mapped types)#
Связанные типы основаны на синтаксисе сигнатуры доступа по индексу, который используется для определения типов свойств, которые не были определены заранее:
Модификаторы связывания (mapping modifiers)#
Повторное связывание ключей с помощью as #
В TS 4.1 и выше, можно использовать оговорку as для повторного связывания ключей в связанном типе:
Для создания новых названий свойств на основе предыдущих можно использовать продвинутые возможности, такие как типы шаблонных литералов (см. ниже):
Ключи можно фильтровать с помощью never в условном типе:
Связанные типы хорошо работают с другими возможностями по манипуляции типами, например, с условными типами. В следующем примере условный тип возвращает true или false в зависимости от того, содержит ли объект свойство pii с литерально установленным true :
Типы шаблонных литералов (template literal types)#
Типы шаблонных литералов основаны на типах строковых литералов и имеют возможность превращаться в несколько строк через объединения.
Когда тип используется в интерполированной позиции, он является набором каждого возможного строкого литерала, который может быть представлен каждым членом объединения:
Для каждой интерполированной позиции в шаблонном литерале объединения являются множественными:
Большие строковые объединения лучше создавать отдельно, но указанный способ может быть полезным в простых случаях.
Строковые объединения в типах#
Мощь шаблонных строк в полной мере проявляется при определении новой строки на основе существующей внутри типа.
Шаблонные литералы предоставляют способ обработки такой операции внутри системы типов:
При передаче неправильного свойства возникает ошибка:
Предположения типов с помощью шаблонных литералов#
Мы можем переписать последний пример с дженериком таким образом, что типы будут предполагаться на основе частей строки eventName :
Здесь мы реализовали on в общем методе.
Внутренние типы манипуляций со строками (intrisic string manipulation types)#
Обобщения (Generics)
Из всего, что стало и ещё станет известным о типизированном мире, тем, кто только начинает свое знакомство с ним, тема, посвященная обобщениям (generics), может казаться наиболее сложной. Хотя данная тема, как и все остальные, обладает некоторыми нюансами, каждый из которых будет детально разобран, в реальности, рассматриваемые в ней механизмы очень просты и схватываются на лету. Поэтому приготовьтесь, к концу главы место занимаемое множеством вопросов, касающихся обобщений, займет желание сделать все пользовательские конструкции универсальными.
К счастью, в нашей реальности нашли решение не только относительно печатных станков, но и типов. Нежелания тратить усилия на постоянное описывание монолитных типов послужило причиной зарождения парадигмы обобщенного программирования.
Обобщенное программирование (Generic Programming) — это подход, при котором алгоритмы могут одинаково работать с данными, принадлежащими к разным типам данных без изменения декларации (описания типа).
В основе обобщенного программирования лежит такое ключевое понятие как обобщение. Обобщение (Generics) — это параметризированный тип позволяющий объявлять параметры типа, являющиеся временной заменой конкретных типов, конкретизация которых будет выполнена в момент создания экземпляра. Параметры типа, при условии соблюдения некоторых правил, можно использовать в большинстве операций, допускающих работу с обычными типами. Все это вместе дает повод сравнивать обобщенный тип с правильной версией печатного станка, чьи заменяемые валы, предназначенные для отпечатывания информации на проходящей через них бумаге, сопоставимы с параметрами типа.
В реальности обобщения позволяют сокращать количество преобразований (приведений) и писать многократно используемый код, при этом повышая его типобезопасность.
Этих примеров должно быть достаточно для образования отчетливого образа обобщений. Но прежде чем продолжить, стоит уточнить значения таких далеко не всем очевидных терминов, как: обобщенный тип, параметризированный тип и универсальная конструкция.
Для понимания этих терминов необходимо представить чертеж бумажного домика, в который планируется поселить пойманного на пикнике жука. Когда гипотетический жук мысленно располагается вне границ начерченного жилища, сопоставимого с типом, то оно предстает в виде обобщенного типа. Когда жук представляется внутри своей будущей обители, то о ней говорят как о параметризированном типе. Если же чертеж материализовался, хотя и в форму представленную обычной коробкой из-под печенья, то её называют универсальной конструкцией.
Другими словами, тип, определяющий параметр, обозначается как обобщенный тип. При обсуждении типов, представляемых параметрами типа, необходимо понимать, что они определены в параметризированном типе. Когда объявление обобщенного типа получило реализацию, то такую конструкцию, будь, то класс или функция, называют универсальной (универсальный класс, универсальная функция или метод).
Обобщения в TypeScript
В TypeScript обобщения могут быть указаны для типов, определяемых с помощью:
Указывается обобщение сразу после идентификатора типа. Это правило остается неизменным даже в тех случаях, когда идентификатор отсутствует (как в случае с безымянным классовым или функциональным выражением), или же и вовсе не предусмотрен (стрелочная функция).
Но прежде чем приступить к детальному рассмотрению, нужно уточнить, что правила для функций, функциональных выражений и методов идентичны. Правила для классов ничем не отличаются от правил для классовых выражений. Исходя из этого, все дальнейшие примеры будут приводиться исключительно на классах и функциях.
В случае, когда обобщение указанно псевдониму типа ( type ), область видимости параметров типа ограничена самим выражением.
Область видимости параметров типа при объявлении функции и функционального выражения, включая стрелочное, а также методов, ограничивается их сигнатурой и телом. Другими словами, параметр типа можно использовать в качестве типа при объявлении параметров, возвращаемого значения, а также в допустимых выражениях (аннотация типа, приведение типа и т.д.) расположенных в теле.
При объявлении классов (в том числе классовых выражений) и интерфейсов, область видимости параметров типа ограничиваются областью объявления и телом.
В случаях, когда класс/интерфейс расширяет другой класс/интерфейс, который объявлен как обобщенный, потомок обязан указать типы для своего предка. Потомок в качестве аргумента типа может указать своему предку не только конкретный тип, но и тип, представляемый собственными параметрами типа.
Если класс/интерфейс объявлен как обобщенный, а внутри него объявлен обобщенный метод, имеющий идентичный параметр типа, то последний в своей области видимости будет перекрывать первый (более конкретно это поведение будет рассмотрено позднее).
Принадлежность параметра типа к конкретному типу данных устанавливается в момент передачи аргументов типа. При этом конкретные типы данных указываются в паре угловых скобок, а количество конкретных типов должно соответствовать количеству обязательных параметров типа.
Если обобщенный тип указывается в качестве типа данных, то он обязан содержать аннотацию обобщения (исключением является параметры типа по умолчанию, которые рассматриваются далее в главе).
Относительно обобщенных типов существуют такие понятия, как открытый (open) и закрытый (closed) тип. Обобщенный тип в момент определения называется открытым.
Кроме того, обобщенные типы, указанные в аннотации у которых хотя бы один из аргументов типа является параметром типа, также являются открытыми типами.
И наоборот, если все аргументы типа принадлежат к конкретным типам, то такой обобщенный тип является закрытым типом.
Те же самые правила применимы и к функциям, но за одним исключением — вывод типов для примитивных типов определяет принадлежность параметров типа к литеральным типам данных.
И опять, эти же правила верны и для функций.
В случаях, когда обобщенный класс содержит обобщенный метод, параметры типа метода будут затенять параметры типа класса.
Стоит заметить, что в TypeScript нельзя создавать экземпляры типов представляемых параметрами типа.
Кроме того, два типа, определяемые классом или функцией, считаются идентичными вне зависимости от того, являются они обобщенными или нет.
Механизм расширения требуется в тех случаях, в которых параметр типа должен обладать заданными характеристиками, необходимыми для выполнения конкретных операций над этим типом.
Для примера рассмотрим случай, когда в коллекции T ( Collection ) объявлен метод получения элемента по имени ( getItemByName ).
Пример, когда параметр типа расширяет другой параметр типа, будет рассмотрен немного позднее.
Также не лишним будет заметить, что когда параметр типа расширяет другой тип, в качестве аргумента типа можно будет передать только совместимый с ним тип.
Кроме того, расширять можно любые подходящие для этого типы, полученные любым доступным путем.
Помимо прочего, TypeScript позволяет указывать для параметров типа значение по умолчанию.
Кроме того, можно совмещать механизм установки значения по умолчанию и механизм расширения типа. В этом случае оператор равно = указывается после расширяемого типа.
В момент, когда тип T расширяет другой тип, он получает признаки этого типа. Именно поэтому для параметра типа, расширяющего другой тип, в качестве типа по умолчанию можно указывать только совместимый с ним тип.
Чтобы было проще понять, нужно представить два класса, один из которых расширяет другой. В этом случае переменной с типом суперкласса можно в качестве значения присвоить объект его подкласса, но — не наоборот.
Тот же самый механизм используется для параметров типа.
Тип, который указывается параметру типа в качестве типа по умолчанию, вообще ничего не ограничивает.
Не будет лишним также рассмотреть отличия этих двух механизмов при работе вывода типов.
Если обобщенная коллекция в качестве аргумента типа получает тип объединение ( Union ), то все её элементы будут принадлежать к типу объединения. Простыми словами, элемент из такой коллекции не будет, без явного преобразования, совместим ни с одним из вариантов, составляющих тип объединение.
Но операцию приведения типов можно поместить (сокрыть) прямо в метод самой коллекции и тем самым упростить её использование. Для этого метод должен быть обобщенным, а его параметр типа, указанный в качестве возвращаемого из функции, расширять параметр типа самой коллекции.
Сокрытие приведения типов прямо в методе коллекции повысило “привлекательность” кода. Но, все же, в случаях, когда элемент коллекции присваивается конструкции без явной аннотации типа, появляется потребность вызывать обобщенный метод с аргументами типа.
Кроме того, нужно не забывать, что два разных объявления параметров типа несовместимы, даже если у них идентичные идентификаторы.