Привет, я Сергей Вахрамов, занимаюсь фронтенд-разработкой на Angular в компании Тинькофф. Во фронтенд-разработку вошел напрямую с тайпскрипта, просто перечитав всю документацию. С того момента и спецификация ECMAScript расширилась, и TypeScript сильно подрос. Казалось бы, почему разработчики могут бояться дженериков, ведь бояться там нечего? Мой опыт общения с джуниор-разработчиками говорит, что во многом ребята не используют обобщенные типы просто потому, что кто-то пустил легенду об их сложности. Эта статья для тех, кто не использует generic-типы в TypeScript: не знают о них, боятся использовать или используют вместо реальных типов — any. Дженерики, или Generic Types, — обобщенные типы. Они нужны для описания похожих, но отличающихся какими-то характеристиками типов. Мы описываем общую структуру, а конкретную уже определяет пользователь дженерика. Дженерик — это каркас, внутренности которого заполняет разработчик. Программист, который описывает обобщенный тип, никогда не знает, что именно туда решит записать тот, кто будет этот тип использовать. Посмотрим на пример использования дженериков в TypeScript. Представьте, что у нас есть массив значков валют. В JavaScript мы бы просто написали: В TypeScript с помощью дженериков можно написать: Здесь важно уделить внимание типу переменной currencySigns — ReadonlyArray, обобщенный тип, означает «неизменяемый массив», при этом мы говорим языку, что в нем могут лежать только элементы типа CurrencySign, это параметр дженерика.  Ничто не запрещает написать ReadonlyArray<'₽' | '€' | '£'>, но часто типы разделяют, чтобы в будущем их было удобно использовать отдельно друг от друга. Например, как в данном случае, было бы удобно заранее иметь тип элемента массива и уже из него сконструировать другой тип, двигаясь «от меньшего к большему», а не выделять из большего типа меньший. Это возможно с помощью декларации infer, но об этом поговорим в другой раз. Это оператор, который берет все ключи объекта и представляет в виде числового или строкового литерального объединения. Давайте представим, что нам с сервера шлют объект с такой структурой: Если нам потребуются ключи из типа Payment, тут и пригодится оператор keyof. В итоге получим: ObjectKey — это дженерик-тип (обобщенный тип), который перечисляет ключи объекта Obj, переданного в него как в параметр. Мы дали основные знания, которые помогут уверенно использовать мощь обобщенных типов. Теперь можно переходить к Generic Types. Случается, что в работе мы используем уже написанное до нас и не влияем на доработку кода.  Представим, что нам с сервера приходит описание платежа из истории в формате: Потом кто-то разработал новый сервис, который отдает информацию о переводах. И он стал передавать информацию в следующем виде: Теперь нам приходит код валюты, а не ее буквенное обозначение. При этом на старые записи в истории мы все еще получаем строковый код. Чтобы не описывать разные типы и не создавать путаницу, можно объединить их в один обобщенный тип — дженерик: Можно указать типы параметров дженерика по умолчанию. Если не передать в такой дженерик параметр, то TypeScript возьмёт значение по умолчанию: Возможно, пример не самый удачный, но он сделан для ввода в курс дела, дальше ждут более приближенные к реальной жизни примеры. Почему T? Так сложилось, что параметры дженериков именуют одной буквой (T означает Type), но вы без проблем можете написать не T, а Currency: При этом, если в IDE мы попытаемся в paymentInfo присвоить полю currency значение типа number, получим ошибку: TypeScript уже охраняет нас. Этого бы не было, если бы тип поля currency был просто string | number. Ведь мы дали возможность разработчику с помощью параметра указать, значение какого типа будет лежать в поле currency. Интерфейсы тоже могут быть обобщёнными: Типы и интерфейсы во многом похожи, но есть и различия — они подробно описаны в документации. Некоторые из функций могут быть вызваны с разным количеством аргументов и их типами. В TypeScript такие функции можно описать с помощью перегрузок. Допустим, у нас есть функция identity — она возвращает аргумент, который мы ей передали. Прошло время, и нам понадобилось сделать такую функцию для boolean. Мы побежим в файл, чтобы добавить еще одну перегрузку: Этот вариант страдает от возможности адекватной расширяемости: если понадобится такая функция identity, которая будет работать с Payments[], нам потребуется добавить еще одну сигнатуру. А если таких типов десяток или сотня? Писать 100 сигнатур — не выход из ситуации. Но можно написать вот так:  В этом примере мы просто типизировали функцию (Function Declaration) через дженерик. Можно также типизировать функциональное выражение (Function Expression): Зачем может понадобиться обобщать классы? Уже на этапе разработки мы часто знаем, что один и тот же класс можно использовать для обслуживания разных данных. Например, выпадающему списку можно дать массив строк для отображения простого контента, а можно передать массив шаблонов для отрисовки сложного. Дженерики помогут описать такой класс. Для упрощения представлю класс IdentityClass. Код на JavaScript: Тот же самый класс будет выглядеть намного понятнее с TypeScript. Для начала опишем интерфейс: Теперь напишем класс, который реализует наш интерфейс: Иногда нужно как-то ограничить тип, принимаемый дженериком. Покажу на реальном примере. Допустим, у нас есть функция для получения значения свойства length аргумента: Если вы попробуете ее скомпилировать, получите ошибку: Происходит это потому, что TypeScript не знает, есть ли у передаваемого аргумента свойство length. Это легко исправить с помощью Generic Constraint — ограничения дженерика. Создадим тип и укажем функции, что при типизации она может принимать только такой тип, который имеет свойство length типа number: Ошибка пропадет, а пример заработает. При этом разработчик, использующий функцию, будет точно понимать, какой аргумент требуется в нее передать. Какой еще может быть практический пример? Давайте напишем функцию, которая принимает в себя объект и ключ, а выдает значение из переданного объекта по ключу: В этом примере также показана возможность ограничения типа, используемого в объявлении функции, с помощью уже имеющегося параметра: Теперь проверим: В основном русскоговорящее сообщество не заморачивается и просто называет их тайп-гардами. Прежде всего сюда можно отнести операторы typeof и instanceof. Они помогают определять и различать типы. Оператор typeof (typeof type guards). TypeScript — классный инструмент. Он позволяет выводить типы из конкретных переменных. Это называется Type Inference или «вывод типов». Допустим, у нас уже есть переменная со ссылкой на объект, в котором записаны данные: Если мы захотим вывести тип этой переменной в коде, то без проблем сделаем это при помощи typeof: В JavaScript тоже есть оператор typeof, который позволяет узнать тип значения, который хранится в переменной. Почему typeof null === ‘object’? Это общепризнанное поведение JS. Многие считают его багом, попытаюсь объяснить почему.  Любая переменная хранит свое значение в виде последовательности битов. Из 32 бит, отведенных для хранения значения переменной, решили 3 выделить под хранение ее типа. Мы понимаем, что всего возможно 2^3 = 8 вариантов типов и, по счастливой случайности, 000 выделили для типа object. Если вы когда-то встречались с понятием «нулевой указатель», то знаете, что он представляет собой переменную — последовательность нулей. Догадываетесь? Проверяя у этой переменной тип, оператор typeof в JavaScript встречает три нулика и понимает, что перед нами объект. Оператор instanceof (instanceof type guards). Позволяет проверить, является ли данный объект экземпляром конкретного класса. Тут ничего сложного. Для упрощения напишем классы без реализации и создадим экземпляры этих классов. В TypeScript есть еще один прекрасный инструмент — «определенные пользователем тайп-гарды». Допустим, у нас есть следующие интерфейсы: Нам нужно написать функцию, которая будет определять, является ли переданное животное объектом, реализующим интерфейс Dog. Если мы напишем в классическом стиле, то это вызовет ряд проблем — TypeScript не будет понимать, что перед ним точно объект, соответствующий интерфейсу Dog: Мы можем легко это исправить. Достаточно лишь сказать TypeScript, что наша функция определяет, реализует ли переданный аргумент интерфейс Dog: Теперь TypeScript не ругается, он понял тип переменной pet: Так же тайп-гард будет работать при условном ветвлении во время проверки наличия поля с помощью оператора in, если тип является объединением, и при этом условное выражение однозначно подразделяется на ветви true и false, это позволяет тайпскрипту однозначно сузить тип внутри этих условных ветвей: Сначала это может сложно восприниматься, поэтому подробнее про сужение типов предлагаю прочитать отдельно в документации. Если искать примеры в реальном мире, то до определенной версии в jQuery невозможно было использовать метод isUndefined, который возвращал значение boolean вместо тайп-гарда ... is undefined. Хотя этот метод в тайпскрипте и был, но разработчики jQuery не описали возвращаемое функцией значение должным образом. Это могло сильно мешать при разработке. Также хочу отметить, что при использовании Type Guard вся ответственность за определение типов лежит на разработчике. Он напрямую говорит TypeScript: «Это точно вот этот тип и никакой другой, я гарантирую». Мы узнали про дженерики и их параметризацию, научились с помощью них типизировать переменные, функции и методы, а также классы. Узнали, как можно ограничить типы и при необходимости помочь TypeScript с выведением типов, использовав Type Guard. Если вы только начинаете использовать TypeScript в своей работе, начните с чтения официальной документации. Ее стоит изучить в первую очередь, просто сев и почитав несколько часов или, возможно, дней. Тогда сложится полное представление о языке. Вряд ли запомнится все и сразу, но уже будете знать, где что искать. Для продвинутых пользователей языка рекомендую официальную книгу — TypeScript Handbook. Ее можно использовать как шпаргалку с паттернами, объяснением поведения системы вывода типов, описанием работы компилятора. Там раскрываются тонкие моменты, о которых разработчик не задумывается повседневно. Если вы «познали дзен» в написании дженериков или просто хотите попрактиковаться на реальных примерах и набить руку, можете также порешать задачки Type Challenges в одноименном github-репозитории. В папке questions задачи разделены по сложности и пронумерованы. Под каждой есть ссылка на предлагаемые разработчиками решения — можете легко проверить себя. Front-end Angular Developer