Универсальные компоненты

Подобно универсальным классам, синтаксис .tsx, позволяет определять React компоненты обобщенными. С этим связано несколько неочевидных моментов, каждый из которых будет рассмотрен в текущей главе.

Обобщенные компоненты (Generics Component)

В TypeScript существует возможность объявлять пользовательские компоненты обобщенными, что лишь повышает их повторное использование. Чтобы избавить читателя от пересказа того, что подробно было рассмотрено в главе “Типы - Обобщения (Generics)”, опустим основную теорию и сосредоточимся конкретно на той её части, которая сопряжена непосредственно с React компонентами. Но поскольку польза от универсальных компонентов может быть не совсем очевидна, прежде чем приступить к рассмотрению их синтаксиса, стоит упомянуть, что параметры типа предназначены по большей степени для аннотирования членов типа представляющего пропсы компонента.

В случае компонентов, расширяющих универсальные классы Component<P, S, SS> или PureComponent<P, S, SS>, нет ничего особенного, на, что стоит обратить особое внимание.

ts
/**[0] */ interface Props<T> { data: T; /**[1] */ } /**[2][3] [4] */ class A<T> extends Component<Props<T>> {} /**[2][3] [4] */ class B<T> extends PureComponent<Props<T>> {} // ...где-то в коде /**[5] */ interface IDataB { b: string; } /**[6] [7] [8] */ <A<IDataA> data={{a: 0}} />; // Ok /**[6] [7] [9] */ <A<IDataA> data={{a: '0'}} />; // Error /**[5] */ interface IDataA { a: number; } /**[6] [7] [8] */ <A<IDataB> data={{b: ''}} />; // Ok /**[6] [7] [9] */ <A<IDataB> data={{b: 0}} />; // Error /** * [0] определение обобщенного типа чей * единственный параметр предназначен для * указания в аннотации типа поля data [1]. * * [2] определение универсальных классовых * компонентов чей единственный параметр типа [3] * будет установлен в качестве аргумента типа типа * представляющего пропсы компонента [4] * * * [5] определение двух интерфейсов представляющих * два различных типа данных. * * [6] создание экземпляра универсального компонента * и установление в качестве пропсов объекты соответствующие [8] * и нет [9] требованиям установленными аргументами типа [7]. */

Нет ничего особенного и в определении функционального компонента как Function Declaration.

ts
/**[0] */ interface Props<T> { data: T; /**[1] */ } /**[2][3] [4] */ function A <T>(props: Props<T>) { return <div></div>; } /** * [0] определение обобщенного типа чей * единственный параметр предназначен для * указания в аннотации типа поля data [1]. * * [2] универсальный функциональный компонент * определенный как Function Declaration [2] чей * единственный параметр типа [3] будет установлен * в качестве аргумента типа типа представляющего * пропсы компонента [4]. * */

Но относительно функциональных компонентов определенных как Function Expression не обошлось без курьезов. Дело в том, что в большинстве случаев лучшим способом описания сигнатуры функционального компонента является использование обобщенного типа FC<P>. Это делает невозможным передачу параметра типа функции в качестве аргумента типа типу представляющему пропсы, поскольку они находятся по разные стороны от оператора присваивания.

ts
interface Props<T> {} const A: FC<Props< /**[0] */ >> = function < /**[1] */ > (props) { return <div></div>; } /** * [0] как получить тут, то... * [1] ..., что объявляется здесь? */

Единственный возможный вариант создания обобщенного функционального компонента определенного как Function Expression заключается в отказе от аннотирования идентификатора в пользу типизирования сигнатуры непосредственно компонента.

ts
interface Props<T> { data: T; } /**[0] [1] [2] */ const A = function <T>(props: Props<T>) { return <div></div>; }; <A<number> data={0}/>; // Ok <A<number> data={''}/>; // Error /** * Чтобы функциональный компонент стал * универсальным определение принадлежности * идентификатора функционального выражения [0] * необходимо поручить выводу типов который * сделает это на основе типов явно указанных * в сигнатуре функции [1] [2] выступающей в качестве * значения. */

Кроме этого, неприятный момент связан со стрелочными универсальными функциями (arrow function) при определении их в файлах имеющих расширение .tsx. Дело в том, что невозможно определить универсальную функцию если она содержит только один параметр типа который не расширяет другой тип.

ts
/**[0] */ const f = <T>(p: T) => {}; /**[1] Error */ [].forEach(/**[2] */<T>() => { }) /**[3] Error */ /** * Не имеет значения присвоена универсальная * стрелочная функция [0] [2] переменной [1] или определена * в месте установления аргумента [3] компилятор * никогда не позволит скомпилировать такой код, если * он расположен в файлах с расширением .tsx */

Другими словами, что бы при определении универсальной стрелочной функции в файле с расширением .tsx не возникало ошибки её единственный параметр типа должен расширять другой тип...

ts
/**[0] */ const f0 = <T extends {}>(p: T) => {}; // Ok /**[0] */ [].forEach(<T extends {}>() => { }); // Ok /** * Если единственный параметр типа * расширяет другой тип [0], то ошибка * не возникает. */

...либо параметров типа должно быть несколько.

ts
/**[0] */ const f0 = <Tб, U>(p: T) => {}; // Ok /**[0] */ [].forEach(<T, U>() => { }); // Ok /** *[0] ошибки также не возникает если универсальная функция определяет несколько параметров типа. */

Для закрепления информации данной главы выполним небольшой пример. Представьте задачу требующую написание компонента испускающего событие, объект которого содержит свойство data хранящего значение переданное вместе с пропсами. Без механизма универсальных компонентов, свойство data, как в пропсах, так и объекте событий, будет представлено либо одним конкретным типом, либо множеством типов составляющих тип объединение.

В первом случае, для каждого типа представляющего данные, потребуется определять новый компонент.

ts
interface DataEvent<T> { data: T; } /**[0] */ interface CardAProps { data: number; /**[1] */ /**[1] */ handler: (event: DataEvent<number>) => void; } /**[2] */ const CardA = ({data, handler}: CardAProps) => { return ( <div onClick={() => handler({data})}>Card Info</div> ); } const handlerA = (event: DataEvent<number>) => {} <CardA data={0} handler={handlerA} /> /** ============== */ /**[3] */ interface CardBProps { data: string; /**[4] */ /**[4] */ handler: (event: DataEvent<string>) => void; } /**[5] */ const CardB = ({data, handler}: CardBProps) => { return ( <div onClick={() => handler({data})}>Card Info</div> ); } const handlerB = (event: DataEvent<string>) => {} <CardB data={``} handler={handlerB} /> /** * [2] [5] определение идентичных по логике компонентов * нужда в которых появляется исключительно из-за необходимости * в указании разных типов [1][4] в описании интерфейсов представляющих * их пропсы [0][3] */

Во втором, для сужения множества типов, придется производить утомительные проверки.

ts
interface DataEvent<T> { data: T; } interface CardProps { data: number | string; /**[0] */ /**[0] */ handler: (event: DataEvent<number | string>) => void; } const Card = ({data, handler}: CardProps) => { return ( <div onClick={() => handler({data})}>Card Info</div> ); } const handler = (event: DataEvent<number | string>) => { // утомительные проверки if(typeof event.data === `number`){ // в этом блоке кода обращаемся как с number }else if(typeof event.data === `string`){ // в этом блоке кода обращаемся как с string } } <Card data={0} handler={handler} /> /** * [0] указание типа как объединение number | string * избавило от необходимости определения множества компонентов, * но не избавила от утомительных и излишних проверок при работе * с данными с слушателе событий. */

Избежать повторяющегося или излишнего кода можно путем определения компонентов универсальными.

ts
interface DataEvent<T> { data: T; } /**[0] */ interface CardProps<T> { data: T; /**[1] */ /**[1] */ handler: (event: DataEvent<T>) => void; } /**[2] [3] [4] */ const Card = function <T>({data, handler}: CardProps<T>) { return ( <div onClick={() => handler({data})}>Card Info</div> ); } const handlerWithNumberData = (event: DataEvent<number>) => {} const handlerWithStringData = (event: DataEvent<string>) => {} <Card<number> data={0} handler={handlerWithNumberData} />; <Card<string> data={``} handler={handlerWithStringData} />; /** * [2] определение универсального функционального компонента * параметр типа которого [3] будет установлен типу представляющего * пропсы [0] в качестве аргумента типа [4], что сделает его описание [1] * универсальным. */

В итоге кода становиться меньше, что делает его проще для чтения. Кроме того, код написанный таким образом более соответствует лучшим канонам обусловленных типизацией.