Универсальные компоненты¶
Подобно универсальным классам, синтаксис .tsx
, позволяет определеять React компоненты обобщенными. С этим свзано несколько неочевидных моментов, каждый из которых будет рассмотрен в текущей главе.
Обобщенные компоненты (Generics Component)¶
В TypeScript существует возможность объявлять пользовательские компоненты обобщенными, что лишь повышает их повторное использование. Чтобы избавить читателя от пересказа того, что подробно было рассмотрено в главе Обобщения (Generics), опустим основную теорию и сосредоточимся конкретно на той её части, которая сопряжена непосредственно с React компонентами. Но поскольку польза от универсальных компонентов может быть не совсем очевидна, прежде чем приступить к рассмотрению их синтаксиса, стоит упомянуть что параметры типа предназначены по большей степени для аннотирования членов типа представляющего пропсы компонента.
В случае компонентов, расширяющих универсальные классы Component<P, S, SS>
или PureComponent<P, S, SS>
, нет ничего особенного, на что стоит обратить особое внимание.
/**[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.
/**[0] */
interface Props<T> {
data: T /**[1] */;
}
/**[2][3] [4] */
function A<T>(props: Props<T>) {
return <div></div>;
}
/**
* [0] определение обобщенного типа чей
* единственный параметр предназначен для
* указания в аннотации типа поля data [1].
*
* [2] универсальный функциональный компонент
* определенный как Function Delaration [2] чей
* единственный параметр типа [3] будет установлен
* в качесте аргумента типа типа представляющего
* пропсы комопнента [4].
*
*/
Но относительно функциональных компонентов определенных как Function Expression не обошлось без курьезов. Дело в том, что в большинстве случаев лучшим способом описания сигнатуры функционального компонента является использование обобщенного типа FC<P>
. Это делает невозможным передачу параметра типа функции в качестве аргумента типа типу представляющему пропсы, поскольку они находятся по разные стороны от оператора присваивания.
interface Props<T> {}
const A: FC<Props</**[0] */>> = function </**[1] */>(
props
) {
return <div></div>;
};
/**
* [0] как получить тут, то...
* [1] ...что объявляется здесь?
*/
Единственный возможный вариант создания обобщенного функционального компонента определенного как Function Expression заключается в отказе от аннотирования идентификатора в пользу типизирования сигнатуры непосредственно компонента.
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
. Дело в том что невозможно определить универсальную функцию если она содержит только один параметр типа который не расширяет другой тип.
/**[0] */
const f = <T>(p: T) => {}; /**[1] Error */
[].forEach(/**[2] */ <T>() => {}); /**[3] Error */
/**
* Не имеет значения присвоена универсальная
* стрелочная функция [0] [2] переменной [1] или определена
* в месте установления аргумента [3] компилятор
* никогда не позволит скомпилировать такой код, если
* он расположен в файлах с расширением .tsx
*/
Другими словами, чтобы при определении универсальной стрелочной функции в файле с расширением .tsx
не возникало ошибки её единственный параметр типа должен расширять другой тип...
/**[0] */
const f0 = <T extends {}>(p: T) => {}; // Ok
/**[0] */
[].forEach(<T extends {}>() => {}); // Ok
/**
* Если единственный параметр типа
* расширяет другой тип [0] то ошибк
* не возникает.
*/
...либо параметров типа должно быть несколько.
/**[0] */
const f0 = <Tб, U>(p: T) => {}; // Ok
/**[0] */
[].forEach(<T, U>() => {}); // Ok
/**
*[0] ошибки также не возникает
если универсальная функция определяет
несколько параметров типа.
*/
Для закрепления информации данной главы выполним небольшой пример. Представьте задачу требующую написание компонента эмитируещего событие, объект которого содержит свойство data
хранящего значение переданное вместе с пропсами. Без механизма универсальных компонентов, свойство data
, как в пропсах, так и объекте событий, будет представлено либо одним конкретным типом, либо множеством типов составляющих тип объединение.
В первом случае, для каждого типа представляющего данные, потребуется определять новый компонент.
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]
*/
Во втором, для сужения множества типов, придется производить утомительные проверки.
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
* избавило от необходимости определения множества компонентов,
* но не избавила от утомительных и излишних проверок при работе
* с данными с слушателе событий.
*/
Избежать повторяющегося или излишнего кода можно путем определения компонентов универсальными.
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]
* универсальным.
*/
В итоге кода становиться меньше, что делает его проще для чтения. Кроме того, код написанный таким образом более соответствует лучшим канонам обусловленных типизацией.