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

Функциональные компоненты

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

Всем известно, что React компоненты, обозначаемые как функциональные, являются обычными функциями. И как все функции в JavaScript, они также могут быть определены двумя способами — в виде обычной функции (Function Declaration) и в виде функционального выражения (Function Expression), таящего один неочевидный нюанс, который подробно будет рассмотрен по ходу знакомства с ним.

Определение компонента как Function Declaration

Типизация параметров

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

/**[0] */
import React from 'react';

/**[1] */
function Timer(/**[2] */) /**[3] */ {
  return <div>Is Timer!</div>;
}

export default Timer;

/**
 * [1] функциональный компонент
 * определенный как Function Declaration.
 * [2] отсутствующие параметры.
 * [3] отсутствие явного указания типа
 * возвращаемого типа.
 * [0] все компоненты обязаны импортировать
 * пространство имен React.
 */

Также необходимо всегда помнить, что независимо от того используется пространство имен React непосредственно или нет, модули определяющие React компоненты обязаны его импортировать. В противном случае компилятор напомнит об этом с помощью ошибки.

/**
 * Error ->
 * 'React' refers to a UMD global, but the current file is a module.
 * Consider adding an import instead.
 */

export function Message() {
  return <span>I ❤️ TypeScript!</span>;
}
import React from 'react';

/**
 * Ok -> добален импорт пространства имен React
 */

export function Message() {
  return <span>I ❤️ TypeScript!</span>;
}

При определении первого параметра функционального компонента props появляется потребность в типе описывающем их.

/**[0] */
interface Props {
  message?: string /**[1] */;
  duration: number /**[2] */;
}

/**[3] */
function Timer({ duration, message = `Done!` }: Props) {
  return <div></div>;
}

/**
 * [0] определения типа интерфейса
 * который будет указан в аннотации
 * первого параметра функционального
 * компонента [3] и чье описание
 * включает необязательное поле message [1]
 *  и обязательного duration [2]
 */

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

// file Informer.tsx

/**[0] */
interface Props {
  message: string;
}

/**
 * [0] описание пропсов компонента Informer
 */

export default function Informer({ message }: Props) {
  return <h1>{message}</h1>;
}

// file InformerDecorator.tsx

import Informer from './Informer';

/**[0] */
interface Props {
  decor: number /**[1] */;
  message: string /**[2] */;
}

/**
 * [0] описание пропсов компонента InformerDecorator
 * [1] значение предназначаемое непосредственно текущему компоненту
 * [2] значение предназначаемое компоненту Informer
 */

export default function InformerDecorator({
  decor,
  message,
}: Props) {
  return <Informer message={message + decor} />;
}

Экспорт типа параметров

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

// file Informer.tsx

/**[0] */ /**[1] */
export interface InformerProps {
  message: string;
}

/**
 * [0] экспорт типа
 * [1] уточнение идентификатора (имени)
 */

export default function Informer({
  message,
}: InformerProps) {
  return <h1>{message}</h1>;
}

// file InformerDecorator.tsx
/**[0] */
import Informer, {
  InformerProps,
} from './Informer'; /**[2] */ /**[3] */

/**
 * [0] импорти типа пропсов
 */

/**[1] */ export interface InformerDecoratorProps
  extends InformerProps {
  decor: number;
  /**[4] */
}

/**
 * [1] экспорт типа
 * [2] уточнение идентификатора (имени)
 * [3] расширение типа пропсов другого компонента
 * позволяет не рописывать необходимые ему поля [4]
 */

export default function InformerDecorator({
  decor,
  message,
}: InformerDecoratorProps) {
  return <Informer message={message + decor} />;
}

В случаях когда компонент-провайдер нуждается только в части пропсов определенных в типе представляющих их, ненужную часть можно исключить с помощью типа Omit<T, K> или Exclude<T, U>.

Получение типа параметров без его экспорта

Тем, кому ближе минимализм, может прийтись по душе подход с получением типа пропсов без его экспорта.

// @filename: Informer.tsx
import React from 'react';

// InformerProps не экспортируется наружу
interface InformerProps {
  message: string;
}

export default function Informer({
  message,
}: InformerProps) {
  return <h1>{message}</h1>;
}
// @filename: InformerDecorator.tsx

/**[0] */
import React, { ComponentType } from 'react';
/**[1] */
import type Informer from './Informer'; /**[3] */ /**[4] */ /**[5] */ /**[6] */ /**[7] */

/**[2] */ type GetProps<T> = T extends ComponentType<
  infer Props
>
  ? Props
  : unknown; /**[9] */

/**[8] */ type InformerProps = GetProps<typeof Informer>;

/**[10] */
export interface InformerDecoratorProps
  extends InformerProps {
  decor: number;
}

/**
 * [0] Импортируем обобщенный тип ComponentType<Props>
 * представляющий объединение классового и функционального
 * компонента. [1] Импортируем как "только тип" функциональный
 * компонент Informer. [2] Определяем тип GetProps<T> параметр типа
 * которого ожидает тип React компонента. Далее, с помощью механизма
 * определения типа на основе условия (условный тип) выясняем принадлежит
 * ли тип [3] T к React компоненту и в этот момент определяем переменную
 * infer Props [5], которая и будет представлять тип пропсов компонент T.
 * Если условие верно, то возвращаем тип [6] Props, иначе unknown.
 * [8] С помощью типа GetProps, на основе типа Informer, полученного
 * с помощью запроса типа [9], определяем новый  тип InformerProps,
 * который в дальнейшем используем по назначению.
 */

Подобный способ будет не заменим при работе со сторонними библиотеками React компонентов, которые не имеют экспорты типов описывающих свои пропсы.

Модификатор readonly для параметров

Не будет лишним напомнить, что при помощи модификатора readonly не удастся избежать изменений переменных ссылки на которые были получены с помощью механизма деструктуризации.

export interface InformerProps {
  /**[0] */
  readonly message: string;
}

/**[1] */
export default function Informer({
  message,
}: Readonly<InformerProps>) {
  message = 'new value'; /**[2] */

  return <h1>{message}</h1>;
}

/**
 * [0] добавление модификатора readonly вручную,
 * а затем ещё тоже самое с помощью тип Readonly<T> [1]
 * и тем не менее переменная message изменяема [2].
 */

Такое поведение является причиной того, что деструктурированные идентификаторы являются определением новой переменной, а переменные не могут иметь модификатор readonly.

/**[0] */
let o: Readonly<{ f: number }> = { f: 0 };
o.f = 1; // Error -> [1]

let { f } = o;
f = 1; // Ok -> [2]

/**
 * Определение переменной o с типом инлайн интерфейса [0]
 * поля которого модифицированны с помощью типа Readonly<T>.
 * При попытке изменить член o.f с модификатором readonly
 * возникает ошибка [1] ->
 * Cannot assign to 'f' because it is a read-only property.
 * Чего не происходит при изменении переменной определенной
 * в процессе деструктуризации.
 *
 * Механизм деструктуризации предполагает создание новой
 * переменной со значением одноименного члена объекта указанного
 * в качестве правого операнда выражения.
 * Выражение let {f} = o; эквивалентно выражению let f = o.f;
 * В этом случае создается новая переменная тип которой устанавливается
 * выводом типов. А вот модификатор readonly не применим к переменным.
 */

Декларирование children

При необходимости декларирования children можно выбрать несколько путей. Первый из них подразумевает использование обобщенного типа PropsWithChildren<P> ожидающего в качестве аргумента типа тип представляющий пропсы. Данный тип определяет children как необязательное поле принадлежащее к ReactNode. При отсутствии продуманного плана на счёт children или необходимости их принадлежности к любому допустимому типу, данный тип будет как нельзя к месту.

/**[0] */
import React, { PropsWithChildren } from 'react';

export interface LabelProps {}

/**[1] */
export default function Label({
  children,
}: PropsWithChildren<LabelProps>) {
  return <span>{children}</span>;
}

/**
 * [0] импорт типа PropsWithChildren<T>
 * для указания его в аннотации типа параметров [1].
 */

<Label>{'label'}</Label>; // string as children -> Ok [2]
<Label>{1000}</Label>; // number as children -> Ok [3]
<Label></Label>; // undefined as chiildren -> Ok [4]

/**
 * При создании экземпляров компонента Label
 * допустимо указывать в качестве children
 * как строку [2], так и числа [3] и кроме
 * того не указывать значения вовсе [4]
 */

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

/**
 * [0] children определены как
 * обязятельное пренадлежащие к
 * типу string поле.
 */
interface LabelProps {
  children: string /**[0] */;
}

export default function Label({ children }: LabelProps) {
  return <span>{children}</span>;
}

<Label>{`label`}</Label>; // Ok
<Label>{1000}</Label>; // Error -> number не совместим со string
<Label></Label>; // Error -> children обязательны

Может показаться, что конкретизация типа children будет полезна при создании собственных ui компонентов. К примеру при создании компонента List выполняющего отрисовку элемента ul, было бы здорово определить children, как массив компонентов ListItem отрисовывающих элемент li.

Для этого понадобится импортировать обобщенный тип ReactElement<P, T>, первый параметр типа которого ожидает тип пропсов, а второй строку или конструктор компонента для указания его в качестве типа поля type необходимого для идентификации. По факту тип ReactElement<P, T> представляет экземпляр любого компонента в системе типов React. После определения компонентов List и ListItem, для первого нам понадобится переопределить поле children, указав ему тип ReactElement<ListItemProps>, что буквально означает - экземпляр компонента, пропсы которого принадлежат к типу указанному в качестве первого аргумента типа.

/**[0] */
import React, { ReactElement, ReactNode } from 'react';

/**
 * [0] импорт типа представляющего
 * экземпляр любого компонента.
 */

interface ListItemProps {
  children: ReactNode /**[1] */;
}

/**
 * [1] для примера определим тип children
 * как ReactNode представляющего любой
 * допустимый тип.
 */

function ListItem({ children }: ListItemProps) {
  return <li>{children}</li>;
}

interface ListProps {
  children: ReactElement<ListItemProps> /**[2] */;
}

/**
 * [2] при определении children
 * указываем тип ReactElement<ListItemProps>
 * что стоит понимать как - экземпляр компонента
 * пропсы которого совм6естимы с типом ListItemProps.
 */

function List({ children }: ListProps) {
  return <ul>{children}</ul>;
}

/**[3] */
<List>
  <ListItem>first</ListItem>
</List>;

/**
 * [3] создаем экземпляр List
 * и указываем ему в качестве children
 * один экземпляр ListItem.
 */

Если кажется просто, то не стоит сомневаться, оно так и есть. Совершенно ничего сложного. Единственное стоит уточнить два важных момента.

Первый момент заключается в том, что конкретизация типа children для React элементов не работает. Проще говоря, если определить новый компонент Label и указать ему в качестве пропсов тип определяющий единственное поле type, то его экземпляр без возникновения ошибки можно будет указать в качестве children компоненту List.

/**[0] */
interface ListItemProps {
  children: string;
}

/**
 * [0] оставляем тип пропсов,
 * но для упрощения удаляем компонент
 * нуждающийся в нем.
 */

/**[1] */
interface LabelProps {
  type: 'danger' | `error`;
  children: ReactNode;
}

/**[1] */
function Label({ type, children }: LabelProps) {
  return <span className={type}>{children}</span>;
}

/**
 * [1] определяем компонент Label
 * и описываем его пропсы.
 */

interface ListProps {
  children: ReactElement<ListItemProps> /**[2] */;
}

/**
 * Тип children по прежнему указан
 * как ReactElement<ListItemProps>.
 */

// компонент List удален для упрощения

/**[3] */
<List>
  <Label type={'danger'}>Hello World!</Label>
</List>;

/**
 * [3] несмотря на то что в комопненте List
 * тип children обозначен как ReactElement<ListItemProps>
 * вместо ожидаемого экземпляра без возникновения
 * ошибки устанавливается тип ReactElement<LabelProps>.
 */

Всё дело в том, что экземпляр компонента представляется типом Element из пространства имен JSX, является производным от типа ReactElement<P, T>. Кроме того, при расширении, своему базовому классу, в качестве аргументов типа, он устанавливает any - Element extends ReactElement<any, any>. Это в свою очередь означает? что любые экземпляры компонентов будут совместимы с любыми типами ReactElement<P, T>, что делает уточнение типа бессмысленным.

let listItem = <ListItem>first</ListItem>; // let listItem: JSX.Element
let label = <Label type={'danger'}>label</Label>; // let label: JSX.Element

/**
 * Поскольку оба экземпляра принадлежат
 * к типу JSX.Element который в свою очередь
 * является производным от типа ReactElement<any, any>,
 * то любой экземпляр будет совместим с любым типом ReactElement<P, T>.
 */

let v0: ReactElement<ListItemProps> = label;
let v1: ReactElement<LabelProps> = listItem;

Кроме этого, ReactElement<P, T> совместим не только с экземплярами компонентов, но и React элементов.

let v: ReactElement<ListItemProps> = <span></span>; // Ok

Это, как уже было сказано, делает конкретизацию children, для экземпляров React компонентов и элементов, бессмысленной. Остается надеяться, что это исправят.

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

interface ListProps {
  children: ReactElement<ListItemProps> /**[0] */;
}

/**[1] */
<List>
  <ListItem>first</ListItem>
  <ListItem>second</ListItem>
</List>;

/**
 * [0] тип указан как единственный
 * экземпляр компонента. поэтому при
 * установке множества экземпляров [1]
 * возникает ошибка.
 */

Для разрешения подобного случая необходимо указать тип children как объединение (Union) определяемое типами представляющих как единственный экземпляр ReactElement<ListItemProps> так и множество, а если быть конкретнее, то массив экземпляров ReactElement<ListItemProps>[].

interface ListProps {
  children:
    | ReactElement<ListItemProps>
    | ReactElement<ListItemProps>[] /**[0] */;
}

/**[1] */
<List>
  <ListItem>first</ListItem>
  <ListItem>second</ListItem>
</List>;

/**
 * [0] указание в качестве типа children
 * объединение предполагающее как единственный
 * экземпляр так и множество, ошибка [1] не возникает.
 */

Ссылки useRef() и ref

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

Во-первых тему посвященную рефам стоит начать с напоминания, что они делятся на два вида. Первый вид предназначен для получения ссылок на React элементы, а второй на React компоненты. Так вот второй параметр функционального компонента предназначен для установления рефы на себя самого, тио есть на компонент.

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

Представьте сценарий при котором форма должна перейти в начальное состояние путем вызова нативного метода reset() что потребует получения на нее ссылки с помощью объекта рефы создаваемого хуком useRef().

Для начала необходимо с помощью универсальной функции useRef() создать объект рефы и присвоить ссылку на него переменной, которую в дальнейшем установить элементу формы. Сразу стоит обратить внимание, что декларации React элементов содержат устаревшие типы в аннотации поля ref. Это непременно приведет к возникновению ошибки при установлении объекта рефы. Чтобы этого избежать, необходимо явным образом, при определении объекта рефы, преобразовать его к обобщенному типу RefObject<T>, которому в качестве аргумента типа установить тип нативного элемента, в данном случае HTMLFormElement. Также стоит сделать акцент на том, что необходимо именно преобразование. Указания аннотации типа переменной или передачи типа в качестве аргумента типа, хуку не поможет. Более детально поведение хука useState() рассматривается в главе посвященной предопределенным хукам.

import React, { useRef, RefObject } from 'react';

function Form() {
  /**[0]         [1]          [2] */
  let formRef = useRef() as RefObject<HTMLFormElement>;

  /**[3] */
  return <form ref={formRef}></form>;
}

/**
 * Создаваемый хуком объект рефы необходимо преобразовать в обобщенный тип
 * RefObject<T> [1] в качестве аргумента которому требуется указать нативный тип
 * формы [2]. Если не произвести преобразования то в момент установки объекта рефы форме [3]
 * возникнет ошибка, поскольку декларации описывавющие React элементы содержут устаревший
 * тип в аннотации поля ref.
 *
 * БОЛЕЕ ПОДРОБНО В ТЕМЕ ПОСВЯЩЕННОЙ ХУКУ useRef()
 *
 */

Обратные ссылки forwardRef

Если появится необходимость задать форме первоначальное состояние извне компонента, то можно прибегнуть к механизму получения ссылки на сам компонент, или точнее на определяемый им объект выступающий в роли публичного api. Но чтобы стало возможным получить ссылку на функциональный компонент, его необходимо преобразовать с помощью универсальной функции forwardRef<R, P>(), на которой сфокусируется дальнейшее повествование.

По факту логика работы универсальной функции forwardRef<R, P>(render) заключатся в проверке единственного параметра на принадлежность к функциональному типу у которой помимо первого параметра представляющего props, определен ещё и второй, представляющий ref. Данная функция обозначается как render и теоретически её можно считать функциональным компонентом с определением второго параметра предназначенного для установления рефы. В системе типов React функция render принадлежит к обобщенному функциональному типу ForwardRefRenderFunction<T, P> определяющего два параметра типа, первый из которых представляет тип рефы, а второй пропсов. Первый параметр функции render представляющий пропсы не таит в себе ничего необычного. Вотличии от него, второй параметр представляющий рефу, требует детального рассмотрения, поскольку именно с ним связан один неочевидный момент.

Дело в том что рефы могут быть представлены как экземпляром объекта принадлежащего к типу RefObject<T> или полностью совместимым с ним MutableRefObject<T>, так и функцией <T>(instance: T) => void. Учитывая этот факт, функция render, в аннотации типа второго параметра ref, просто вынуждена указать все эти типы в качестве union. Но сложность состоит в том, что определение объединения происходит непосредственно в аннотации типа параметра ref. Простыми словами система типов React не предусмотрела более удобного и короткого псевдонима типа представляющего рефу определяемую функциональным компонентом.

interface ForwardRefRenderFunction<T, P = {}> {
  /**                              [0] */
  (
    props: PropsWithChildren<P>,
    ref:
      | ((instance: T | null) => void)
      | MutableRefObject<T | null>
      | null
  ): ReactElement | null;
}

/**
 * [0] тип объединение определенный
 * непосредственно в аннотации типа.
 * Простыми словами он не имеет более
 * удобного короткого псевдонима.
 */

Это означает, что функциональный компонент определенный как Function Declaration и указавший принадлежность второго параметра к типу, скажем MutableRefObject<T> не сможет пройти проверку на совместимость типов в качестве аргумента универсальной функции forwardRef(), даже если установить её аргументы типа. И причина тому контрвариантность параметров функции при проверке на совместимость.

/**[0]           [1] */
import React, { MutableRefObject, forwardRef } from 'react';

export interface FormProps {}

/**[2] */
export interface FormApi {
  reset: () => void;
}

/**
 * [2] объявление типа описывающего доступное
 * api компонента.
 */
/**[3]       [4]         [5] */
function Form(
  props: FormProps,
  ref: MutableRefObject<FormApi>
) {
  return null;
}

/**[6]          [7]      [8]       [9]     [10] */
const FormWithRef = forwardRef<FormApi, FormProps>(
  Form /**Error */
);

export default FormWithRef; /**[11] */

/**
 * [0] импорт обобщенного типа MutableRefObject<T>
 * который будет указан в аннотации типа [4] второго
 * параметра [3] функионального компонента предварительно
 * получив в качестве аргумента типа тип нативного dom
 * элемента HTMLDivElement [5].
 *
 * Несмотря на все принятые меры по типизации сигнатуры функционального
 * компонента Form избежать возникновения ошибки [10] при проверке на совместимость
 * в момент передачи в качестве аргумента универсальной функции forwardRef [7] не получится
 * даже при конкретизации с помощью аргументов функционального типа [8][9].
 *
 * [11] для экспорта функционального компонента определяющего второй параметр необходимо
 * сохранить результат выполнения функции forwardRef [6].
 */

Разрешить данную ситуацию можно несколькими способами. Первый заключается в явном преобразовании типа функционального компонента к типу ForwardRefRenderFunction<T, P> которому в качестве аргументов типа требуется указать необходимые типы. При этом отпадает нужда в указании аргументов типа непосредственно самой универсальной функции forwardRef<T, P>();

/**[0] */
import React, {
  MutableRefObject,
  forwardRef,
  ForwardRefRenderFunction,
} from 'react';

export interface FormProps {}
export interface FormApi {
  reset: () => void;
}

function Form(
  props: FormProps,
  ref: MutableRefObject<FormApi>
) {
  return null;
}

/**[6]                          [7]       [8]        [9] */
const FormWithRef = forwardRef(
  Form as ForwardRefRenderFunction<FormApi, FormProps>
);

export default FormWithRef;

/**
 * [0] импорт обобщенного функционального типа ForwardRefRenderFunction<T, P>
 * к которому тип Form [7] будет преобразован с помощью оператора as, для
 * чего потребуентся указать необходимые аргументы типа [8] [9]. При этом отпадает
 * потребность в установке аргументов непосредственно универсальной функции [6]
 */

Следующий способ заключается в получении типа представляющего рефу непосредственно из самого функционального типа ForwardRefRenderFunction<T, P>. Для необходимо указать в аннотации второго параметра функционального компонента обобщенный тип взятого у второго параметра функционального типа ForwardRefRenderFunction<T, P> при помощи типа Parameters<T> предназначенного для получения массива с типами соответствующих параметрам функции. Поскольку интересующий нас тип принадлежит второму параметру, то он будет доступен как элемент под индексом один. Кроме того в указании аргументов типа универсальной функции forwardRef<T, P>() нет необходимости, поскольку выводу типов достаточно описания сигнатуры функционального компонента.

import React, {
  forwardRef,
  ForwardRefRenderFunction,
} from 'react';

export interface FormProps {}
export interface FormApi {
  reset: () => void;
}

/**[0][1]       [2]             [3]            [4] [5] */
type Ref<T> = Parameters<ForwardRefRenderFunction<T>>[1];

/**[6]   [7] */
function Form(props: FormProps, ref: Ref<FormApi>) {
  return null;
}

/**[8] */
const FormWithRef = forwardRef(Form);

export default FormWithRef;

/**
 * При помощи типа Parameters<T> [2] получаем массив элементы которого
 * принадлежат к типам параметров функции представляемой типом
 * ForwardRefRenderFunction<T, P> [3] которому в качестве первого аргумента
 * типа [4] устанавливаем параметр обобщенного псевдонима [1]. Таким образом
 * Ref<T> ссылается на первый элемент массива [5] содержащего тип указанный в аннотации
 * второго параметра (ref). Определенный псевдоним указываем в аннотации
 * второго параметра функционального компонента [6] установив в качестве аргумента
 * типа тип нативного dom элемента [7]. При таком сценарии нет необходимости
 * конкретизировать типы при помощи аргументов типа универсальной функции
 * forwardRef<T, P>() [8].
 */

Последнее на что стоит обратить внимание, это обобщенный тип ForwardRefExoticComponent<P> к которому принадлежит значение возвращаемое из универсальной функции forwardRef<T, P>() и указывать который в явной форме нет никакой необходимости.

/**[0] */
import React, {
  forwardRef,
  ForwardRefRenderFunction,
  ForwardRefExoticComponent,
} from 'react';

// ...

/**[1]                [2] */
const FormWithRef: ForwardRefExoticComponent<FormProps> = forwardRef(
  Form
);

export default FormWithRef;

/**
 * [0] импорт обобщенного типа ForwardRefExoticComponent<P>
 * который в качестве аргумента типа ожидает тип представляющий пропсы того
 * компонента [2] в аннотации типа которого указан [1]. СТоит заметить что
 * указан он исключительно в образовательных целях.
 */

После того как функциональный компонент получит рефу, ему необходимо присвоить ей значение выступающее в качестве открытого api. Для этого необходимо прибегнуть к помощи хука useImperativeHandle<T, R>(ref: Ref<T>, apiFactory() => R): void подробное рассмотрение которого можно найти в теме посвященной предопределенным хукам.

Для того чтобы ассоциировать api компонента с компонентной рефой при помощи хука useImperativeHandle(), ему необходимо передать её в качестве первого аргумента. После этого, компонентная рефа будет ассоциирована со значением возвращаемого из функции ожидаемой хуком в качестве второго параметра. Процесс переинициализации компонентной рефы будет выполнятся всякий раз при изменении элементов массива ожидаемого в качестве третьего параметра данного хука. Также необходимо уточнить что рефа создаваемая с помощью хука useRef() и предназначенная для ассоциации с функциональным компонентом, также нуждается в явном преобразовании к обобщенному типу MatableRefObject<T>, которому в качестве единственного аргумента типа будет установлен тип представляющий открытое api компонента.

import React, {
  forwardRef,
  ForwardRefRenderFunction,
  RefObject,
  useRef,
  useImperativeHandle,
  MutableRefObject,
} from 'react';

interface FormProps {}
interface FormApi {
  reset: () => void;
}

type Ref<T> = Parameters<ForwardRefRenderFunction<T>>[1];

function Form(props: FormProps, ref: Ref<FormApi>) {
  /**[0] */
  let formRef = useRef() as RefObject<HTMLFormElement>;

  /**[1]          [2]     [3] */
  useImperativeHandle(
    ref,
    () => ({
      reset: () => formRef.current?.reset(),
    }),
    [] /**[4] */
  );

  return <form ref={formRef}></form>;
}

/**
 * [0] не забываем о необходимости явного преобразования.
 * Хук useImperativeHandle жидает в качестве первого параметра
 * ссылку [1], в качестве второго фабричную функцию [3] которая
 * будет переопредлелять объект api каждый раз при изменении
 * элементов массива ожидаемого в качестве третьего параметра [4].
 *
 */

const FormWithRef = forwardRef(Form);

const App = () => {
  /**[5]         [6]        [7] */
  let formRef = useRef() as MutableRefObject<FormApi>;

  /**[8] */
  formRef.current?.reset();

  /**[9] */
  return <FormWithRef ref={formRef} />;
};

/**
 * Необходимо помнить что ссылка предназначенная
 * для ассоцииации с функциональным компонентом
 * также требует явного преобразование [5] к обобщенному
 * типу MutableRefObject<T> [6] которому в качестве аргумента
 * типа необходимо установить тип представляющий открытое api
 * компонента [7]. И после создания экземпляра компонента определенного
 * с помощью функции forwardRef [9] можно использовать его api через объект
 * рефы [8].
 */

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

import React from 'react';

export interface TimerProps {}

export default function Timer(props: TimerProps) /**[0] */ {
  return <div>Is Timer!</div>;
}

/**
 * [0] отсутствие явного указания типа
 * возвращаемого типа, что для большинства
 * случаев является ппредпочтительным.
 */

Система типов React

И дело не в том чтобы как можно больше делегировать работы выводу типов экономя тем самым драгоценное время, а в том, что система типов React, устанавливаемая из репозиториев @types, не имеет достаточно высокого уровня типобезопастности. Поскольку это очень щекотливая тема её освещение стоит начать с самого начала, а именно с перечисления типов к которым может принадлежать возвращаемое значение.

И так, любое допустимое возвращаемое компонентом значение, в системе типов React, может быть представлено типом ReactNode являющимся объединением (Union) определяемого типами ReactChild | ReactFragment | ReactPortal | boolean | null | undefined. Тип ReactChild также представляет собой объединение типов ReactElement<Props, Type> | ReactText. Первый, как уже было рассмотрено ранее, представляет экземпляр любого компонента и элемента React, а второй объединение string | number. ReactFragment представляет объединение для {} | ReactNodeArray. Не сложно догадаться, что ReactNodeArray, это абстракция над Array<ReactNode>. Оставшийся тип ReactPortal является производным от типа ReactElement. Это может казаться очень запутанным и более того разбираться в этом прямо сейчас нет нужды, поскольку совсем скоро станет ясно, в чем кроется подвох, причиной которого являются два из перечисленных типа.

Первый тип, вносящий смуту, это ранее рассмотренный ReactElement<P, T> и всё неожиданное поведение которое с ним связанно. Вторым типом вносящий неразбериху стал ReactFragment, поскольку определяющий его пустой объектный тип {} совместим с любым экземпляром объектного типа. По факту, при использовании в качестве типа возвращаемого значения ReactFragment или ReactNode ошибки не возникнет даже если оно будет экземпляром Promise или чего-то ещё. И хотя отсутствие ошибки на этапе компиляции не означает, что её получится избежать во время выполнения, сам сценарий с возвратом ошибочного значения может показаться чересчур надуманным. С какой-то долей вероятности можно с этим согласится, но поскольку идеология TypeScript подразумевает выявление проблем в программах до их запуска, об этом стоило хотя бы упомянуть.

import React, { ReactFragment, ReactNode } from 'react';

function A(): ReactFragment {
  return Promise.resolve(0); /**[0] */
}
function B(): ReactNode {
  return Promise.resolve(0); /**[0] */
}

/**
 * [0] Ok на этапе компиляции и
 * Error во время выполнения.
 */

/**[1] */
class T {
  constructor(readonly p: number) {}
}

function C(): ReactFragment {
  return new T(0); /**[2] */
}
function D(): ReactNode {
  return new T(0); /**[2] */
}

/**
 * [1] определение некоторого класса.
 * [2] Ok на этапе компиляции и
 * Error во время выполнения.
 */

Из всего этого следует что прибегать к аннотации типа возвращаемого значения стоит только в случаях когда оно принадлежит к number, string, boolean, null или массиву элементы которого принадлежат к оному из четырех перечисленных типов. Да и то при острой необходимости. В остальных случаях целесообразней возложить эту работу на вывод типов, для которого это обычное дело.

Типы событий React

Последнее что осталось без внимания, это событийный механизм или если быть точнее определение слушателей событий. Для этого в системе типов React определен специальный обобщенный тип ReactEventHandler<T> ожидающий в качестве аргумента типа тип представляющий нативный dom элемент которому будет установлен текущий слушатель событий.

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

Первым делом реализация подобного сценария потребует импорта обобщенного типа ReactEventHandler<T>, который в качестве аргумента типа получит тип нативного dom элемента HTMLFormElement, после чего будет указан в аннотации слушателя событий form_submitHandler. Выбор типа нативного dom элемента, в данном случае HTMLFormElement, обуславливается типом элемента, которому устанавливается слушатель событий, в данном случае <form>.

Стоит также обратить внимание что единственный параметр слушателя событий в аннотации типа не нуждается, поскольку вывод типов опирается на обобщенный тип ReactEvenHandler<T>.

По возникновению события, первым делом необходимо предотвратить поведение по умолчанию, чтобы избежать перезагрузки вкладки браузера. Поскольку ссылка на нативную форму доступна через определенное в объекте события свойство target, которое принадлежит к типу EventTarget, то перед присвоением её переменной form появляется необходимость в приведении к типу HTMLFormElement с помощью оператора as. После это можно вызывать нативный метод reset.

/**[0] */
import React, { ReactEventHandler } from 'react';

function Form() {
  /**[1]                 [2]              [3]           [4] */
  const form_submitHandler: ReactEventHandler<HTMLFormElement> = (
    event
  ) => {
    event.preventDefault(); /**[5] */

    /**  [6]        [7]              [8] */
    let form = event.target as HTMLFormElement;
    form.reset(); /**[9] */
  };

  return (
    /**[10] */
    <form onSubmit={form_submitHandler}>
      <button type="submit">submit</button>
    </form>
  );
}

/**
 * [0] импорт обобщенного функционального типа
 * которому установив в качестве аргумента типа
 * тип нативного элемента HTMLFormElement [3]
 * использовали в аннотации типа [2] переменной
 * form_su0bmitHandler [1], которой в качестве
 * значения присвоили функцию слушатель события
 * единственный параметр которой не нуждается в
 * явной аннотации типа [4], поскольку вывод типов
 * операется на тип ReactEventHandler<T>.
 *
 * При возникновении события первым делом происходит
 * предотвращение поведения по умолчанию чтобы избежать
 * перезагрузки вкладки браузера [5]. Затем создается
 * переменная form [6] которой присваивается ссылка на
 * нативный dom элемент доступный через свойство определенное
 * в объекте события target [7] которое при помощи оператора
 * as приведено к нужному типу нативного dom элемента HTMLFormElement [8].
 *
 * На следующем шаге вызывается нативный метод сброса значений формы reset [9]
 *
 * [10] установка слушателя событий React элементу form.
 *
 */

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

Для этого, в нашем конкретном случае, необходимо импортировать обобщенный тип FormEvent<T>, которому перед размещением в аннотации единственного параметра слушателя события необходимо в качестве аргумента события указать тип нативного dom элемента HTMLFormElement. Также стоит напомнить что в аннотации возвращаемого из слушателя события значения нет необходимости. Подобную рутинную работу необходимо делегировать выводу типов.

/**[0] */
import React, { FormEvent } from 'react';

function Form() {
  /**[1]      [2]           [3]          [4] */
  const form_submitHandler = (
    event: FormEvent<HTMLFormElement>
  ): void => {};

  return (
    <form onSubmit={form_submitHandler}>
      <button type="submit">submit</button>
    </form>
  );
}

/**
 * [0] импорт обобщенного типа FormEvent<T>
 * которому перед добавлением в аннотацию типа [2]
 * единственного параметра слушателя события [1]
 * необходимо установить в качестве аргумента типа
 * тип нативного dom элемента HTMLFormElement [3].
 * Указании типа к которому принадлежит возвращаемое
 * из слушателя события значения было указанно лишь
 * для того чтобы напомнить об отсутствии в этом необходимости.
 * Подобную работу нужно делегировать выводу типов.
 */

Работа непосредственно с формой обусловила выбор более конкретного типа события, каковым в данном примере стал обобщенный тип FormEvent<T>. При других условиях потребуются другие событийные типы. Кроме того, всегда можно сделать выбор в пользу базового для всех событийных типов SyntheticEvent<T> ожидающего в качестве аргумента типа тип нативного dom элемента.

Мемоизация слушателей событий

Кроме этого, функциональным компонентам доступна мемоизация слушателей событий при помощи универсального хука useCallback<T>(). Для этого понадобится импортировать универсальную функцию определяющую два обязательных параметра. В качестве первого параметра ожидается функция чье описание устанавливается в качестве аргумента функционального типа. Второй параметр принадлежит к типу массива, изменение элементов которого приводит к переинициализации функции переданной в качестве первого параметра. Поскольку в качестве аргумента функционального тпа ожидается тип описывающий первый параметр хука, то нет необходимости в аннотациях типа её параметров. Или в данном случае её единственного параметра представляющего объект события. В остальном, реализация ничем не отличается от предыдущего примера, поэтому повторяющийся код будет исключён.

/**[0] */
import React, {
  useCallback,
  ReactEventHandler,
} from 'react';

function Form() {
  /**[1]                    [2]                       [3]*/
  const form_submitHandler = useCallback<
    ReactEventHandler<HTMLFormElement>
  >((event) => {}, [] /**[4] */);

  return (
    <form onSubmit={form_submitHandler}>
      <button type="submit">submit</button>
    </form>
  );
}

/**
 * [0] импорт универсальной функции useCallback<T>()
 * принимающей в качестве первого обязательного параметра
 * функцию [3], описание которой устанавливается в качестве
 * аргумента функционального типа [1]. D качестве второго
 * обязательного параметра ожидается массив [4] со значениями
 * изменение которых приводит переинициализации функции переданной
 * в качестве первого аргумента.
 */

На этом рассмотрение типизирования функционального компонента определенного как Function Declaration, завершено. И поскольку тема получилась довольно не маленькая исключим затягивание и перейдем к рассмотрению следующего вида функциональных компонентов.

Определение компонента как Function Expression

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

Для определения функционального компонента как Function Expression декларация типов React предусматривает вспомогательный обобщенный тип FC<Props>, чей идентификатор (имя) является сокращением от Function Component, а аргумент типа представляет пропсы и является необязательным. Поскольку вывод типов ориентируется на тип пропсов указанный или присущий по умолчанию в качестве аргумента типа, то в аннотировании первого параметра функционального компонента нет надобности. Помимо этого, тип пропсов, по умолчанию описывает необязательное поле children принадлежащего к типу ReactNode.

/**[0] */
import React, { FC } from 'react';

/**[1]       [2] */
const Timer: FC = ({ children }) => <div>Is Timer!</div>;

/**
 * [0] импорт обобщенного типа FC<P>
 * который указан в аннотации без
 * установки аргумента типа [1] и
 * несмотря на это ошибки при деструктуризации
 * поля children не возникает даже без аннотации
 * типа первого параметра [2]
 */

Если тип пропсов указан в качестве аргумента типа FC<P> и при этом не описывает поле children, то оно всё равно будет определенно в объекте пропсов доступного в качестве первого параметра функционального компонента.

import React, { FC } from 'react';

export interface TimerProps {
  duration: number;
  /**[0] */
}
/**[1]                     [3]*/
const Timer: FC<TimerProps> = ({ duration, children }) => (
  <div>Is Timer!</div>
);

export default Timer;

/**
 * [0] несмотря на то что тип представляющий
 * пропсы и указанный в качестве аргумента
 * типа FC<P> [1] не описывает поле children
 * при их деструктуризации ошибки не возникает [3]
 */

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

При возникновении необходимости в определении второго параметра функционального компонента придется самостоятельно указывать аннотацию типа, поскольку по каким-то причинам обобщенный тип FC<P> этого не предусматривает.

import React, { FC } from 'react';

export interface TimerProps {}

/**[1]   [2] */
const Timer: FC<TimerProps> = (props, ref) => (
  <div>Is Timer!</div>
);

/**
 * Первый параметр [0] выводится как
 * PropsWithChildren<TimerProps>, а
 * второй [2] как any, поскольку обобщенный
 * тип FC<P> не предусматривает его наличие.
 */

В остальном все рассмотренное относительно рефов в теме посвященной функциональным компонентам определенным как Function Declaration, верно и для текущего вида определения.

При необходимости во втором параметре можно отказаться от типа FC<P> в пользу ранее рассмотренного типа ForwardRefRenderFunction<T, P>. При указании данного типа в аннотации функционального компонента, пропадает необходимость, как в явном аннотировании типов его параметров, так и в указании аргументов типа универсальной функции forwardRef<T, P>().

import React, {
  FC,
  forwardRef,
  ForwardRefRenderFunction,
} from 'react';

export interface TimerProps {}

/**[1] */
const Timer: ForwardRefRenderFunction<
  HTMLDivElement,
  TimerProps
> = (props, ref) => <div>Is Timer!</div>;

/**[2] */
const TimerWithRef = forwardRef(Timer);

export default TimerWithRef;

/**
 * [0] импорт типа для указания его в аннотации
 * функционального компонента [1] определяющего второй
 * параметр ref. После этого нет необходимости в явной
 * аннотации типов как обоих параметров функционального
 * компонента, так и универсальной функции forwardRef<T, P>().
 */

Важной особенностью использования обобщенного типа FC<P> заключается в том, что он, помимо типа представляющего пропсы, также содержит описание типа возвращаемого функцией значения. Вроде бы так и должно быть, но нюанс заключается в том, что возвращаемое значение обязательно должно принадлежать к типу совместимому с ReactElement<P, T>. Простыми словами на этапе компиляции возникнет ошибка если функциональный компонент, определенный как Fuction Expression, будет возвращать значение принадлежащие к типам number, string, boolean или абсолютно любому массиву.

import React, { FC } from 'react';

const A: FC = () => 0123; // Error
const B: FC = () => '0123'; // Error
const C: FC = () => true; // Error
const D: FC = () => []; // Error

const E: FC = () => <div></div>; // Ok
const F: FC = () => <E />; // Ok
const G: FC = () => <></>; // Ok

Поэтому в случае, предполагающем, что функциональный компонент, определенный как Function Expression, будет возвращать значение отличное от ReactElement<P, T>, потребуется самостоятельно описать его сигнатуру. Что не представляет никакого труда.

import React, { MutableRefObject } from 'react';

export interface TimerProps {}

const Timer = (
  props: TimerProps,
  ref: MutableRefObject<HTMLDivElement>
) => 123;

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

Для этого потребуется определить обобщенный тип с двумя необязательными параметрами. Первый необязательный параметр представляющий тип пропсов должен расширять и к тому же указывать в качестве типа по умолчанию тип object. Второй необязательный параметр типа должен проделать тот же процесс только для нативного типа HTMLElement.

Чтобы не заморачиваться в определении children, указываем принадлежность первого параметра функции к уже знакомому обобщенному типу PropsWithChildren<P>, которому в качестве аргумента типа устанавливаем первый параметр типа P. Второму необязательному параметру функции указываем принадлежность к обобщенному типу MutableRefObject<E> в качестве аргумента типа которому устанавливаем второй параметр типа E. Осталось лишь указать принадлежность возвращаемого функцией значения к типу ReactNode и тип CFC<P, E>, что является сокращением от Custom Functional Component, готов сэкономить время и нервы разработчика.

// file CFC.ts

import React, {
  MutableRefObject,
  ReactNode,
  PropsWithChildren,
} from 'react';

/**[0][1]         [2]     [3][4]           [5]            [6]                       [7]                      [8]             [9]*/
export type CFC<
  P extends object = object,
  E extends HTMLElement = HTMLElement
> = (
  props: PropsWithChildren<P>,
  ref?: MutableRefObject<E>
) => ReactNode;

/**
 * [0] определяем обобщенный тип CustomDunctionComponent
 * или сокращенно CFC первый необязательный параметр представляющего
 * пропсы которого [1] расширяет [2] и устанавливает по умолчанию [3]
 * тип object. Второй необязательный параметр пердставляющий тип нативного
 * dom элемента [4] расширяет [5] и устанавливает по умолчанию [6] тип HTMLElement.
 *
 * [7] устанавливаем принадлежность первого параметра функционального типа к
 * обобщенному типу PropsWithChildren<P> которому в качестве аргумента типа передаем первый
 * параметр типа.
 *
 * [8] определяем принадлежность второго необязательного параметра к обобщенному типу
 * MutableRefObject<E> которому в качестве аргумента типа устанавливаем второй параметр типа.
 *
 * [9] тип возвращаемого значения определяем как ReactNode.
 *
 */

// file Timer.tsx
import React from 'react';
import { CFC } from './CFC'; /**[0] */

export interface TimerProps {}

/**[1]    [2]           [3]            [4]   [5]     [6]*/
const Timer: CFC<TimerProps, HTMLDivElement> = (
  props,
  ref
) => 123;

/**
 * [0] импорт CustomFunctionCOmponent для
 * указания его в качестве типа функционального
 * компонента определенного как Function Expression [1].
 * В качестве первого параметра типа устанавливается тип
 * представляющий пропсы [2], а в качестве второго тип нативного
 * dom элемента с которым будет ассоциирован объект реф [3].
 * При таком подходе отпадает необходимость в явном указании аннотации
 * типов как пропсов [4], так и рефы [5]. Кроме того возвращаемое значение
 * может принадлежать к любому типу совместимому с типом ReactNode [6]
 */

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