Тайная техника Хокаге: Полиморфизм и Дженерики в TypeScript
-
Всем здрасьте!
Сегодня хочу разобрать две важные темы, которые тесно пересекаются между собой, а именно - Полиморфизм и Дженерики (обобщенные типы).Поехалииииииии
Дженерики и полиморфизм являются связанными концепциями в программировании, особенно в контексте объектно-ориентированного программирования (ООП) и языков, поддерживающих статическую типизацию, таких как TypeScript. Однако они не являются одним и тем же понятием; каждый из них имеет свои особенности и применения.
Дженерики
Дженерики — это механизм, позволяющий писать код, который может работать с различными типами данных без потери типовой безопасности. Они используются для создания универсальных компонентов, таких как коллекции, функции и классы, которые могут работать с любыми типами данных, сохраняя при этом строгую проверку типов.
function identity<T>(arg: T): T { return arg; }
Здесь T является переменной типа параметра, которая позволяет функции identity принимать аргументы любого типа и возвращать значение того же типа.
Полиморфизм
Полиморфизм — это способность системы использовать объекты разных типов, но с похожими интерфейсами, так, чтобы эти объекты могли взаимодействовать друг с другом без необходимости изменения их внутренней реализации. Полиморфизм позволяет программисту писать более гибкий и модульный код, поскольку различные классы могут реализовывать один и тот же интерфейс по-разному.
class Animal { speak() { console.log('Это животинка'); } } class Pig extends Animal { speak() { console.log('Хрюшка делает: хрю хрю'); } }
Здесь Animal является базовым классом с методом speak, а Pig является подклассом, который переопределяет этот метод, предоставляя свою уникальную реализацию.
Ну а теперь о главном
Что такое конкретные типы в TypeScript - это:
// Пример если что boolean; string; Date[]; {a: number} | {b: string}; (number: number[]) => number;
Конкретные типы полезны, но полезны когда Вы точно знаете какой ожидается тип и хотите сверить его с переданным типом. Но иногда это нецелесообразно ограничивать поведение функций конкретным типом.
Давай представим с Вами функцию
filter
для итерации по массиву и его очистки.
В JS она может выглядеть примерно так:function filter(array, f) { let result = [] for (let i = 0; i < array.length; i++) { let item = array[i] if(f(item)) { result.push(item) } } } filter([1, 2, 3, 4], _ => _ <= 3) // вычисляется как [1, 2]
А теперь давайте приступим с извлечения сигнатуры типа filter и добавления временных заместителей
unknown
для типов:type Filter = { (array: unknown, f: unknown) => unknown[] }
Далее попробуем заполнить типы, к примеру возьмем
number
:type Filter = { (array: number[], f: (item: number) => boolean): number[] }
Такая сигнатура будет работать для массива чисел, но не как не строк и уже тем более не объектов и других массивов. Давайте теперь попробуем использовать перегрузку для ее расширения:
type Filter = { (array: number[], f: (item: number) => boolean): number[] (array: string[], f: (item: string) => boolean): string[] }
Пока вроде все отлично, но прописывать перегрузку для каждого типа - такая себе идея.
А может еще жахнем массив объектов ?type Filter = { (array: number[], f: (item: number) => boolean): number[] (array: string[], f: (item: string) => boolean): string[] (array: object[], f: (item: object) => boolean): object[] }
В принципе выглядит неплохо, но если реализовать функцию
filter
с сигнатуройfilter: Filter
и ее использовать, то получим следующее:let names = [ {firstName: 'Lox'}, {firstName: 'Debik'}, {firstName: 'Dodik'}, ] let result = filter( names, _ => _.firstName.startsWith('L') ) // Ошибка TS2339: свойство 'firstName' не существует в типе 'object'. result[0].firstName // Ошибка TS2339: свойство 'firstName' //не существует в типе 'object'.
В этом месте должно стать понятно, почему TypeScript выдает ошибку.
Мы сообщили ему, что можем передать массив чисел, строк или объектов вfilter
, и передали массив объектов. Но как Вы уже должны знать,object
ничего не сообщает TypeScript о конкретной форме самого объекта.И что же делать спросите вы?
Если ранее вы писали на языке, который поддерживает обобщенные типы, то наверняка хрюкните “ХОЧУ ОБОБЩЕННЫЕ ТИПЫ”!
Замечательная новость в том, что вы правы, а вот плохая - вы только что разбудили @JspiДля несведущих начну с определения обобщенных типов, а затем приведу пример с нашей функцией.
ПАРАМЕТР ОБОБЩЕННОГО ТИПА
Замещающий тип, используемый для применения ограничений на уровне типов в нескольких местах. Также известен как параметр полиморфного типа.
Едем дальше. Вот как будет выглядеть тип
filter
, если мы перепишем его с параметром обобщенного типаT
:type Filter = { <T>(array: T[], f: (item: T) => boolean): T[] }
Таким образом мы сообщили: “Функция
filter
использует параметр обобщенного типаT
. Мы заранее не знаем, каким будет тот или ной тип в дальнейшем, поэтому, TypeScript, если ты сможешь сделать его вывод при каждом вызовеfilter
, то было бы замечательно”.
TypeScript выводит типT
на основе типа, который мы передаем дляarray
. Как только TypeScript делает вывод, чем являетсяT
для вызоваfilter
, он подставляет этот тип для каждого видимогоT
.
T
выступает в роли замещающего типа, который заполняется модулем проверки на основе контекста. Он параметризует типFilter
, поэтому мы и зовем его параметром обобщенного типа.“Фраза параметр обобщенного типа часто заменяется на обобщенный тип или обобщение.”
Вообще для объявления обобщенных типов используются угловые скобки
(<>)
А еще их называют ДЖЕНЕРИКАМИ (Generics), воспринимайте их как ключевое словоtype
.
Место размещения скобок определяет диапазон охватываемых типов (да, кстати есть всего несколько мест где вы можете их использовать). TypeScript в свою очередь убеждается, что внутри этого диапазона все экземпляры параметров обобщенных типов привязаны к одному реальному типу. В текущем примере при вызовеfilter
TypeScript привяжет конкретные типы к обобщенному типуT
в зависимости от обозначенного скобками диапазона. Какой именно тип привязывать кT
, он решит исходя из того, с каким типом мы вызовемfilter
. Между угловых скобок мы можем объявить столько обобщений, сколько пожелаем, разделив их точкой с запятой.T - это просто имя типа, такое же как А, LOX, baloven, 2k24. Но в мире TS принято использовать имена состоящие из одной заглавной буквы, так что всего скорее в чужом коде вы встретите такие имена типов как T, U, V, W и т.п.
Если конечно вы используете множество типов или какие-то сложные образы то конечно лучше стоит рассмотреть вариант вроде Value или WidgetType.
А еще некоторые программисты предпочитают начинать с А вместо Т. Это зависит от сообщества пользователей разных языков программирования которые делают выбор согласно устоявшейся традиции. Например функциональщики предпочитают A, B, C. Разработчики ООП склонны использовать Т или type. TypeScript поддерживает любой стиль, но предпочтительнее используется последний
ПРОДОЛЖАЕМ!
Подобно тому как параметр функции повторно привязывается при каждому вызове функции, также и каждый вызовfilter
получает свою привязку для T:type Filter = { <T>(array: T[], f: (item: T) => boolean): T[] } let filter: Filter = (array, f) => // ... // (a) T привязан к number filter([1,2,3], _=>_>2) // (b) T привязан к строке filter(['a', 'b'], _=>_!=='b') // (c) T привязан к {firstName: string} let names = [ {firstName: 'lox'}, {firstName: 'baloven'}, {firstName: 'kapysha'} ] filter(names, _=>_.firstName.startsWith('b'))
TypeScript делает вывод привязок обобщенных типов на основе типов переданных аргументов. Глянем, как привязывает T для (a):
- Исходя из сигнатуры
filter
TypeScript знает, что array - это массив элементов некоего типа Т. - TypeScript замечает, что мы передали в массив
[1, 2, 3]
, а значит Т должен бытьnumber
- Везде, где TypeScript видит Т, он заменяет его на
number
. Следовательно параметрf: (item: T) => boolean
становитсяf: (item: number) => boolean
, а возвращаемый тип T[] становится number[]. - TypeScript проверяет типы на совместимость и убеждается, что функция, которую мы передали как f, совместима со своей только что выведенной сигнатуры.
Обобщенные типы - это эффективный способ выразить более обширное действие функции, чем это позволяет конкретный тип. Воспринимать же их стоит в виде ограничений. Как аннотирование параметра функции в виде
n: number
ограничивает значение параметраn
типомnumber
, так и использование обобщенного типа Т ограничивает тип любого привязываемого к Т условием типа быть одинаковым в каждом Т.Обобщенные типы также могут применяться в псевдонимах типов, классах и интерфейсах.
Ну а на этом я предлагаю завершить данную статью и ознакомление с так называемыми Дженериками.
P.S. В последующих статьях я хочу более подробно рассмотреть как привязывать конкретные типы к обобщенным, а также где можно их объявлять.
- Исходя из сигнатуры