Тайная техника Хокаге: Полиморфизм и Дженерики в 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 в свою очередь убеждается, что внутри этого диапазона все экземпляры параметров обобщенных типов привязаны к одному реальному типу. В текущем примере при вызовеfilterTypeScript привяжет конкретные типы к обобщенному типу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):
- Исходя из сигнатуры
filterTypeScript знает, что 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. В последующих статьях я хочу более подробно рассмотреть как привязывать конкретные типы к обобщенным, а также где можно их объявлять.
- Исходя из сигнатуры
© 2024 - 2025 ExLends, Inc. Все права защищены.