- Реакт Компоненты-дженерики
- Базовые понятия
- Как пишутся компоненты-дженерики
- Какие задачи решают
- Полезность на личном опыте
- Заключение
- Дженерики в TypeScript: разбираемся вместе
- Дженерики в TypeScript
- Обобщённые классы и интерфейсы
- Реальные случаи использования: выходим за рамки примитивных типов
- Подведем итоги
Реакт Компоненты-дженерики
Однажды мне понадобилось написать гибкий на типизацию компонент в React. Мне нужно было, чтобы в зависимости от одного пропса в виде массива элементов, менялся другой пропс, а точнее аргумент рендер функции, которую я тоже должен передавать в качестве пропса. В обычных функциях тс, я бы подобную проблему решил через дженерики, и поэтому у меня появилась идея написать компонент дженерик.
Базовые понятия
Прежде чем написать свой первый компонент-дженерик, нужно понять и научиться писать обычные Тайпскрипт Дженерики.
Для чего вообще нужны дженерики? Они нужны для того, чтобы мы могли использовать наши конструкции кода не только с заранее заданными типами, но и иметь вариативность.
Проще написать, к сожалению, не получилось, поэтому предлагаю разобраться на примере.
Давайте представим, что у нас есть какая-то функция, которая имитирует запрос к серверу и отдает нам значение в виде обертки над нашим аргументом.
function request(arg: string) < return < status: 200, data: arg >>
По коду выше наша функция будет иметь тип
function request(arg: string):
Все, что написано выше — это, конечно, здорово, но давайте представим, что мы хотим работать с этой фукнцией не только с аргументом строкой.
interface Response < status: number; data: unknown; >function request(arg: unknown): Response < return < status: 200, data: arg >>
function request(arg: number) < return < status: 200, data: arg >>
Что нам даст использование дженерика
function request(arg: T) < return < status: 200, data: arg >> // Если мы вызовем request с числом request(100) // То request будет иметь тип function request(arg: number): < status: number; data: number; >// А если со строкой request('100'); // То function request(arg: string):
Если хотите более подробно познакомиться с дженериками, можете посмотреть документацию
Как пишутся компоненты-дженерики
class CustomComponent extends React.Component < // . >
function CustomComponent (props: React.PropsWithChildren): React.ReactElement< // . >
const CustomComponent = (props: React.PropsWithChildren: React.ReactElement => < // . >
Какие задачи решают
Давайте рассмотрим полезность на простом примере
Что же нам даст использование этого компонента на деле
const AnotherCustomComponent: React.FC = () => < const data = ['text', 'text', 'text']; return ( onClick=* */> /> ) >
в примере выше CustomComponent будет ожидать в onClick функцию (element: string) => void так как в data был передан массив строк
Теперь давайте рассмотрим пример, где в качестве data у нас будет не массив примитивов, а массив сущностей
class User < constructor( public name: string, public lastName: string )<>get fullName() < return `$$` > > const AnotherCustomComponent: React.FC = () => < const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( * в он клик нам не придется ручками подставлять тип. Как в примере выше, он сам выведется из того, что было передано в data */> onClick= alert(user.fullName)> /> ) >
теперь функция onClick будет иметь тип (element: User) => void
Давайте рассмотрим немного другой пример
Так как мы в data передавали массив юзеров children будет иметь тип (element: User) => React.ReactElement
По аналогии с дженериками-функциями в дженерики-компоненты можно явно определить нужный тип, с которым мы бы хотели работать, но такой синтаксис я редко использую
const AnotherCustomComponent: React.FC = () => < const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( * тут мы явно задаем что мы хотим работать с юзером */> data= onClick= alert(user)> > < // тут тип как в onClick тоже высчитается (user) =>< return Пользователь: > > ) > const AnotherCustomComponent: React.FC = () => < const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( * тут мы явно задаем что мы хотим работать со строкой и при передаче в data массив юзеров будет ошибка */> data= onClick= alert(user)> > < (user) =>< return Пользователь: > > ) > 

Полезность на личном опыте
Реальные примеры из жизни не стал прикладывать, так как мои компоненты довольно жирненькие, поэтому решил показать полезность на псевдо примерах, но в качестве бонуса решил словами описать последнюю проблему, которую решил через компонент-дженерик.
Мне нужно было сделать списки для товаров и категорий, которые можно было драг-н-дропать, для изменения порядка отображения, и функционал у этих двух списков идентичен, кроме отображения. Я написал один компонент, который мог работать с драг-н-дропом, у него был пропс children который являлся рендер функцией для элемента, что мне и позволило менять отображение списка при одинаковом функционале.
Заключение
В заключении хотелось бы сказать, что компоненты-дженерики хоть и довольно нишевы, но бывают очень полезны. Надеюсь моя статья будет очень полезна и поможет вам написать свой первый компонент-дженерик.
Дженерики в TypeScript: разбираемся вместе

Наверное, только матёрые разработчики Java или других строго типизированных языков не хлопают глазами, увидев дженерик в TypeScript. Его синтаксис коренным образом отличается от всего того, что мы привыкли видеть в JavaScript, поэтому так непросто сходу догадаться, что он вообще делает.
Я бы хотел показать вам, что на самом деле всё гораздо проще, чем кажется. Я докажу, что если вы способны реализовать на JavaScript функцию с аргументами, то вы сможете использовать дженерики без лишних усилий. Поехали!
Дженерики в TypeScript
В документации TypeScript приводится следующее определение: «дженерики — это возможность создавать компоненты, работающие не только с одним, а с несколькими типами данных».
Здорово! Значит, основная идея состоит в том, что дженерики позволяют нам создавать некие повторно используемые компоненты, работающие с различными типами передаваемых им данных. Но как это возможно? Вот что я думаю.
Дженерики и типы соотносятся друг с другом, как значения и аргументы функции. Это такой способ сообщить компонентам (функциям, классам или интерфейсам), какой тип необходимо использовать при их вызове так же, как во время вызова мы сообщаем функции, какие значения использовать в качестве аргументов.
Лучше всего разобрать это на примере дженерика тождественной функции. Тождественная функция — это функция, возвращающая значение переданного в неё аргумента. В JavaScript она будет выглядеть следующим образом:
function identity (value) < return value; >console.log(identity(1)) // 1
Сделаем так, чтобы она работала с числами:
function identity (value: Number) : Number < return value; >console.log(identity(1)) // 1
Отлично, мы добавили в определение тождественной функции тип, но хотелось бы, чтобы она была более гибкой и срабатывала для значений любого типа, а не только для чисел. Именно для этого и нужны дженерики. Они позволяют функции принимать значения любого типа данных на входе и, в зависимости от них, преобразовывать саму функцию.
function identity (value: T) : T < return value; >console.log(identity(1)) // 1
Ох уж этот странный синтаксис ! Отставить панику. Мы всего лишь передаём тип, который хотим использовать для конкретного вызова функции.

Посмотрите на картинку выше. Когда вы вызываете identity(1) , тип Number — это такой же аргумент, как и 1. Он подставляется везде вместо T . Функция может принимать несколько типов аналогично тому, как она принимает несколько аргументов.

Посмотрите на вызов функции. Теперь-то синтаксис дженериков не должен вас пугать. T и U — это просто имена переменных, которые вы назначаете сами. При вызове функции вместо них указываются типы, с которыми будет работать данная функция.
Альтернативная версия понимания концепции дженериков состоит в том, что они преобразуют функцию в зависимости от указанного типа данных. На анимации ниже показано, как меняется запись функции и возвращаемый результат при изменении типа.
Как можно видеть, функция принимает любой тип, что позволяет создавать повторно используемые компоненты различных типов, как и было обещано в документации.
Обратите особое внимание на второй вызов console.log на анимации выше — в него не передаётся тип. В этом случае TypeScript попытается вычислить тип по переданным данным.
Обобщённые классы и интерфейсы
Вам уже известно, что дженерики — это всего лишь способ передать типы в компонент. Только что вы видели, как они работают с функциями, и у меня хорошие новости: с классами и интерфейсами они работают точно таким же образом. В этом случае указание типов следует после имени интерфейса или класса.
Посмотрите на пример и попробуйте разобраться сами. Надеюсь, у вас получилось.
interface GenericInterface < value: U getIdentity: () =>U > class IdentityClass implements GenericInterface < value: T constructor(value: T) < this.value = value >getIdentity () : T < return this.value >> const myNumberClass = new IdentityClass(1) console.log(myNumberClass.getIdentity()) // 1 const myStringClass = new IdentityClass("Hello!") console.log(myStringClass.getIdentity()) // Hello!
Если код сразу не понятен, попробуйте отследить значения type сверху вниз вплоть до вызовов функции. Порядок действий следующий:
- Создаётся новый экземпляр класса IdentityClass , и в него передаются тип Number и значение 1 .
- В классе значению T присваивается тип Number .
- IdentityClass реализует GenericInterface , и нам известно, что T — это Number , а такая запись эквивалентна записи GenericInterface .
- В GenericInterface дженерик U становится Number . В данном примере я намеренно использовал разные имена переменных, чтобы показать, что значение типа переходит вверх по цепочке, а имя переменной не имеет никакого значения.
Реальные случаи использования: выходим за рамки примитивных типов
Во всех приведённых выше вставках кода были использованы примитивные типы вроде Number и string . Для примеров самое то, но на практике вы вряд ли станете использовать дженерики для примитивных типов. Дженерики будут по-настоящему полезны при работе с произвольными типами или классами, формирующими дерево наследования.
Рассмотрим классический пример наследования. Допустим, у нас есть класс Car , являющийся основой классов Truck и Vespa . Пропишем служебную функцию washCar , принимающую обобщённый экземпляр Car и возвращающую его же.
class Car < label: string = 'Generic Car' numWheels: Number = 4 horn() < return "beep beep!" >> class Truck extends Car < label = 'Truck' numWheels = 18 >class Vespa extends Car < label = 'Vespa' numWheels = 2 >function washCar (car: T) : T < console.log(`Received a $in the car wash.`) console.log(`Cleaning all $ tires.`) console.log('Beeping horn -', car.horn()) console.log('Returning your car now') return car > const myVespa = new Vespa() washCar(myVespa) const myTruck = new Truck() washCar(myTruck)
Сообщая функции washCar , что T extends Car , мы обозначаем, какие функции и свойства можем использовать внутри этой функции. Дженерик также позволяет возвращать данные указанного типа вместо обычного Car .
Результатом выполнения данного кода будет:
Received a Vespa in the car wash. Cleaning all 2 tires. Beeping horn - beep beep! Returning your car now Received a Truck in the car wash. Cleaning all 18 tires. Beeping horn - beep beep! Returning your car now
Подведем итоги
Надеюсь, я помог вам разобраться с дженериками. Запомните, всё, что вам нужно сделать, — это всего лишь передать значение type в функцию 🙂
Если хотите ещё почитать про дженерики, я прикрепил далее пару ссылок.
Что почитать: