Подобно универсальным классам, синтаксис .tsx, позволяет определять Reactкомпоненты обобщенными. С этим связано несколько неочевидных моментов, каждый из которых будет рассмотрен в текущей главе.
В TypeScript существует возможность объявлять пользовательские компоненты обобщенными, что лишь повышает их повторное использование. Чтобы избавить читателя от пересказа того, что подробно было рассмотрено в главе Обобщения (Generics), опустим основную теорию и сосредоточимся конкретно на той её части, которая сопряжена непосредственно с React компонентами. Но поскольку польза от универсальных компонентов может быть не совсем очевидна, прежде чем приступить к рассмотрению их синтаксиса, стоит упомянуть что параметры типа предназначены по большей степени для аннотирования членов типа представляющего пропсы компонента.
В случае компонентов, расширяющих универсальные классы Component<P, S, SS> или PureComponent<P, S, SS>, нет ничего особенного, на что стоит обратить особое внимание.
/**[0] */interfaceProps<T>{data:T/**[1] */;}/**[2][3] [4] */classA<T>extendsComponent<Props<T>>{}/**[2][3] [4] */classB<T>extendsPureComponent<Props<T>>{}// ...где-то в коде/**[5] */interfaceIDataB{b:string;}/**[6] [7] [8] */<A<IDataA>data={{a:0}}/>;// Ok/**[6] [7] [9] */<A<IDataA>data={{a:'0'}}/>;// Error/**[5] */interfaceIDataA{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.
1 2 3 4 5 6 7 8 910111213141516171819202122
/**[0] */interfaceProps<T>{data:T/**[1] */;}/**[2][3] [4] */functionA<T>(props:Props<T>){return<div></div>;}/** * [0] определение обобщенного типа чей * единственный параметр предназначен для * указания в аннотации типа поля data [1]. * * [2] универсальный функциональный компонент * определенный как Function Delaration [2] чей * единственный параметр типа [3] будет установлен * в качесте аргумента типа типа представляющего * пропсы комопнента [4]. * */
Но относительно функциональных компонентов определенных как Function Expression не обошлось без курьезов. Дело в том, что в большинстве случаев лучшим способом описания сигнатуры функционального компонента является использование обобщенного типа FC<P>. Это делает невозможным передачу параметра типа функции в качестве аргумента типа типу представляющему пропсы, поскольку они находятся по разные стороны от оператора присваивания.
1 2 3 4 5 6 7 8 9101112
interfaceProps<T>{}constA:FC<Props</**[0] */>>=function</**[1] */>(props){return<div></div>;};/** * [0] как получить тут, то... * [1] ...что объявляется здесь? */
Единственный возможный вариант создания обобщенного функционального компонента определенного как Function Expression заключается в отказе от аннотирования идентификатора в пользу типизирования сигнатуры непосредственно компонента.
1 2 3 4 5 6 7 8 9101112131415161718192021
interfaceProps<T>{data:T;}/**[0] [1] [2] */constA=function<T>(props:Props<T>){return<div></div>;};<A<number>data={0}/>;// Ok<A<number>data={''}/>;// Error/** * Чтобы функциональный компонент стал * универсальным определение принадлежности * идентификатора функционального выражения [0] * необходимо поручить выводу типов который * сделает это на основе типов явно указанных * в сигнатуре функции [1] [2] выступающей в качестве * значения. */
Кроме этого, неприятный момент связан со стрелочными универсальными функциями (arrow function) при определении их в файлах имеющих расширение .tsx. Дело в том что невозможно определить универсальную функцию если она содержит только один параметр типа который не расширяет другой тип.
1 2 3 4 5 6 7 8 9101112
/**[0] */constf=<T>(p:T)=>{};/**[1] Error */[].forEach(/**[2] */<T>()=>{});/**[3] Error *//** * Не имеет значения присвоена универсальная * стрелочная функция [0] [2] переменной [1] или определена * в месте установления аргумента [3] компилятор * никогда не позволит скомпилировать такой код, если * он расположен в файлах с расширением .tsx */
Другими словами, чтобы при определении универсальной стрелочной функции в файле с расширением .tsx не возникало ошибки её единственный параметр типа должен расширять другой тип...
1 2 3 4 5 6 7 8 91011
/**[0] */constf0=<Textends{}>(p:T)=>{};// Ok/**[0] */[].forEach(<Textends{}>()=>{});// Ok/** * Если единственный параметр типа * расширяет другой тип [0] то ошибк * не возникает. */
...либо параметров типа должно быть несколько.
1 2 3 4 5 6 7 8 91011
/**[0] */constf0=<Tб,U>(p:T)=>{};// Ok/**[0] */[].forEach(<T,U>()=>{});// Ok/** *[0] ошибки также не возникает если универсальная функция определяет несколько параметров типа. */
Для закрепления информации данной главы выполним небольшой пример. Представьте задачу требующую написание компонента эмитируещего событие, объект которого содержит свойство data хранящего значение переданное вместе с пропсами. Без механизма универсальных компонентов, свойство data, как в пропсах, так и объекте событий, будет представлено либо одним конкретным типом, либо множеством типов составляющих тип объединение.
В первом случае, для каждого типа представляющего данные, потребуется определять новый компонент.
interfaceDataEvent<T>{data:T;}/**[0] */interfaceCardAProps{data:number/**[1] */;/**[1] */handler:(event:DataEvent<number>)=>void;}/**[2] */constCardA=({data,handler}:CardAProps)=>{return(<divonClick={()=>handler({data})}>CardInfo</div>);};consthandlerA=(event:DataEvent<number>)=>{};<CardAdata={0}handler={handlerA}/>;/** ============== *//**[3] */interfaceCardBProps{data:string/**[4] */;/**[4] */handler:(event:DataEvent<string>)=>void;}/**[5] */constCardB=({data,handler}:CardBProps)=>{return(<divonClick={()=>handler({data})}>CardInfo</div>);};consthandlerB=(event:DataEvent<string>)=>{};<CardBdata={``}handler={handlerB}/>;/** * [2] [5] определение идентичных по логике компонентов * нужда в кторых появляется исключительно из-за необходимости * в указании разных типов [1][4] в описании интерфейсов представляющих * их пропсы [0][3] */
Во втором, для сужения множества типов, придется производить утомительные проверки.
interfaceDataEvent<T>{data:T;}interfaceCardProps{data:number|string/**[0] */;/**[0] */handler:(event:DataEvent<number|string>)=>void;}constCard=({data,handler}:CardProps)=>{return(<divonClick={()=>handler({data})}>CardInfo</div>);};consthandler=(event:DataEvent<number|string>)=>{// утомительные проверкиif(typeofevent.data===`number`){// в этом блоке кода обраащаемся как с number}elseif(typeofevent.data===`string`){// в этом блоке кода обраащаемся как с string}};<Carddata={0}handler={handler}/>;/** * [0] указание типа как объединение number | string * избавило от необходимости определения множества компонентов, * но не избавила от утомительных и излишних проверок при работе * с данными с слушателе событий. */
Избежать повторяющегося или излишнего кода можно путем определения компонентов универсальными.
interfaceDataEvent<T>{data:T;}/**[0] */interfaceCardProps<T>{data:T/**[1] */;/**[1] */handler:(event:DataEvent<T>)=>void;}/**[2] [3] [4] */constCard=function<T>({data,handler}:CardProps<T>){return(<divonClick={()=>handler({data})}>CardInfo</div>);};consthandlerWithNumberData=(event:DataEvent<number>)=>{};consthandlerWithStringData=(event:DataEvent<string>)=>{};<Card<number>data={0}handler={handlerWithNumberData}/>;<Card<string>data={``}handler={handlerWithStringData}/>;/** * [2] определение универсального функционального компонента * парметр типа которого [3] будет установлен типу представляющего * пропсы [0] в качестве аргумента типа [4], что сделает его описание [1] * универсальным. */
В итоге кода становиться меньше, что делает его проще для чтения. Кроме того, код написанный таким образом более соответствует лучшим канонам обусловленных типизацией.