Предисловие
Однажды мне понадобилось написать гибкий на типизацию компонент в React. Мне нужно было, чтобы в зависимости от одного пропса в виде массива элементов, менялся другой пропс, а точнее аргумент рендер функции, которую я тоже должен передавать в качестве пропса. В обычных функциях тс, я бы подобную проблему решил через дженерики, и поэтому у меня появилась идея написать компонент дженерик.
Базовые понятия
Прежде чем написать свой первый компонент-дженерик, нужно понять и научиться писать обычные Тайпскрипт Дженерики.
Для чего вообще нужны дженерики? Они нужны для того, чтобы мы могли использовать наши конструкции кода не только с заранее заданными типами, но и иметь вариативность.
Проще написать, к сожалению, не получилось, поэтому предлагаю разобраться на примере.
Давайте представим, что у нас есть какая-то функция, которая имитирует запрос к серверу и отдает нам значение в виде обертки над нашим аргументом.
function request(arg: string) { return { status: 200, data: arg } }
По коду выше наша функция будет иметь тип
function request(arg: string): { status: number; data: string; }
Все, что написано выше - это, конечно, здорово, но давайте представим, что мы хотим работать с этой фукнцией не только с аргументом строкой.
Есть 3 варианта...
Использовать тип unknown или any
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<T>(arg: T) { return { status: 200, data: arg } }
Что нам даст использование дженерика
function request<T>(arg: T) { return { status: 200, data: arg } } // Если мы вызовем request с числом request(100) // То request будет иметь тип function request<number>(arg: number): { status: number; data: number; } // А если со строкой request('100'); // То function request<string>(arg: string): { status: number; data: string; }
Если хотите более подробно познакомиться с дженериками, можете посмотреть документацию
Как пишутся компоненты-дженерики
Классовые:
class CustomComponent<T> extends React.Component<T> { // ... }
Функциональные:
function CustomComponent<T> (props: React.PropsWithChildren<T>): React.ReactElement{ // ... }
const CustomComponent = <T, >(props: React.PropsWithChildren<T>: React.ReactElement => { // ... }
Какие задачи решают
Давайте рассмотрим полезность на простом примере
interface CustomComponentProps<T> { data: T[] onClick: (element: T) => void; } class CustomComponent<T> extends React.Component<CustomComponentProps<T>> { render() { return ( <div> <h1>Список</h1> <ul> { this.props.data.map((element) => ( <li onClick={() => this.props.onClick(element)}></li> )) } </ul> </div> ) } }
Что же нам даст использование этого компонента на деле
const AnotherCustomComponent: React.FC = () => { const data = ['text', 'text', 'text']; return ( <div> <CustomComponent data={data} onClick={/* */} /> </div> ) }
в примере выше CustomComponent будет ожидать в onClick функцию (element: string) => void так как в data был передан массив строк

Теперь давайте рассмотрим пример, где в качестве data у нас будет не массив примитивов, а массив сущностей
class User { constructor( public name: string, public lastName: string ){} get fullName() { return `${this.name} ${this.lastName}` } } const AnotherCustomComponent: React.FC = () => { const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( <div> {/* в он клик нам не придется ручками подставлять тип. Как в примере выше, он сам выведется из того, что было передано в data */} <CustomComponent data={data} onClick={(user) => alert(user.fullName)} /> </div> ) }
теперь функция onClick будет иметь тип (element: User) => void

Давайте рассмотрим немного другой пример
interface CustomComponentProps<T> { data: T[] onClick: (element: T) => void; // делаем children рендер функцией для элемента списка children: (element: T) => React.ReactElement } class CustomComponent<T> extends React.Component<CustomComponentProps<T>> { render() { return ( <div> <h1>Список</h1> <ul> { this.props.data.map((element) => ( <li onClick={() => this.props.onClick(element)}>{this.props.children(element)}</li> )) } </ul> </div> ) } } class User { constructor( public name: string, public lastName: string ){} get fullName() { return `${this.name} ${this.lastName}` } } const AnotherCustomComponent: React.FC = () => { const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( <div> <CustomComponent data={data} onClick={(user) => alert(user)}> { // тут тип как в onClick тоже высчитается (user) => { return <div>Пользователь: {user.fullName}</div> } } </CustomComponent> </div> ) }
Так как мы в data передавали массив юзеров children будет иметь тип (element: User) => React.ReactElement

По аналогии с дженериками-функциями в дженерики-компоненты можно явно определить нужный тип, с которым мы бы хотели работать, но такой синтаксис я редко использую
Пример валидного кода:
const AnotherCustomComponent: React.FC = () => { const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( <div> {/* тут мы явно задаем что мы хотим работать с юзером */} <CustomComponent<User> data={data} onClick={(user) => alert(user)} > { // тут тип как в onClick тоже высчитается (user) => { return <div>Пользователь: {user.fullName}</div> } } </CustomComponent> </div> ) }
Пример не валидного кода:
const AnotherCustomComponent: React.FC = () => { const data = [ new User('Джон', 'Сина'), new User('Дуэйн', 'Джонсон'), new User('Дейв', 'Батиста'), ]; return ( <div> {/* тут мы явно задаем что мы хотим работать со строкой и при передаче в data массив юзеров будет ошибка */} <CustomComponent<string> data={data} onClick={(user) => alert(user)} > { (user) => { return <div>Пользователь: {user.fullName}</div> } } </CustomComponent> </div> ) }


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