Перейти к содержанию

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

Подобно универсальным классам, синтаксис .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 = <, 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]
 * универсальным.
 */

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