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

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

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