Данная статья рассчитана на тех, кто только начинает писать свои React приложения на TypeScript, а также является памяткой для меня, ведь совсем недавно я путался в типизации children props.

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

Если же чувствуете, что несмотря на то, что в проекте нет сложной логики, но дерево компонентов и количество и вариация передаваемых пропсов внушительные, я бы воспользовался prop-types. Ранее эта фича входила в состав React и использовали её так: React.PropTypes. Но начиная с версии React 15.5 она переехала в отдельную библиотеку, поэтому теперь её необходимо устанавливать как, например, npm/yarn пакет. Она используется для валидации типов props в компонентах React. Это всё из возможностей TS, но для проекта с большим количеством компонентов и пропсов - то, что нужно. Синтаксис описания типов пропсов отличается от TS.

Типизация пропсов через prop-types

import PropTypes from 'prop-types';

const Component = ({ name, age, isActive }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <p>Active: {isActive ? 'Yes' : 'No'}</p>
  </div>
);

Component.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
  isActive: PropTypes.bool,
};

export default Component;

Типы PropTypes

  • PropTypes.any: Любое значение.

  • PropTypes.bool: Булевое значение.

  • PropTypes.number: Число.

  • PropTypes.string: Строка.

  • PropTypes.func: Функция.

  • PropTypes.array: Массив.

  • PropTypes.object: Объект.

  • PropTypes.symbol: Символ.

  • PropTypes.node: Что-то, что можно отрендерить (число, строка, элемент, массив и т.д.).

  • PropTypes.element: React элемент.

  • PropTypes.instanceOf(Class): Экземпляр определенного класса.

  • PropTypes.oneOf(['Option1', 'Option2']): Один из указанных значений.

  • PropTypes.oneOfType([PropTypes.string, PropTypes.number]): Один из указанных типов.

  • PropTypes.arrayOf(PropTypes.number): Массив элементов определенного типа.

  • PropTypes.objectOf(PropTypes.number): Объект со значениями определенного типа.

  • PropTypes.shape({ name: PropTypes.string, age: PropTypes.number }): Объект с заданной структурой.

  • PropTypes.exact({ name: PropTypes.string, age: PropTypes.number }): Объект с точно заданной структурой (дополнительные свойства запрещены).

TypeScript

Плавно переходим к TypeScript. Его пора подключать, когда сложность и количество логики растет, а также много данных начинают приходить с backend. Когда все данные получаем с backend, тут точно только TypeScript. Да, он не поможет в продакшене как-то обработать не тот тип данных, который попал в props. Его мощь в другом - у вас упадет проект на этапе компиляции.

Термины runtime и compile time

Есть два процесса выполнения кода:

  • Runtime (это когда мы открыли в среде выполнения (например, в браузере) наше приложение, оно начинает отрисовываться, а также когда пользователь начинает с ним взаимодействовать - через навешанные обработчики событий).

  • Compile time (это когда при разработке, для преобразования TS в JS, запускается TS сервер, и момент преобразования (компиляции) TS кода в JS называется compile time. Как правило, при разработке в dev режиме compile time запускается после сохранения ts/tsx файла с изменениями. Еще в IDE постоянно происходит парсинг кода, и когда мы начинаем передавать не тот тип во что-то затипизированное (например, в компонент с затипизированными пропсами) - редактор кода подсвечивает переменную, которая имеет тип отличный от ожидаемого).

Так вот, при ошибках в PropTypes у нас проект не падает, а проблемы выводятся в консоль. К явным преимуществам работы на TypeScript относительно PropTypes добавлю возможность интеграции с IDE: подсказки типов и автозаполнение кода. Как правило, работают у всех по умолчанию после установки TypeScript. Еще лучшее документирование кода: типы служат документацией для props компонентов. Это крайне важно для проектов с кучей HTTP запросов. Типизировать в TS можно всё, не только props, что крайне важно для больших и сложных проектов, где есть глобальные переменные (enum в помощь), расширение типов (и интерфейсов), сложносоставные типы (utility types), динамические типы (Generics). Без всех этих инструментов сложное и при этом надежное приложение не построить. Если описать одним предложением, зачем нужен в проекте TS, я бы ответил: с TypeScript поддержка, развитие и рефакторинг кода становится гораздо менее рискованным и более предсказуемым.

В TS типизацию props принято описывать в интерфейсе (можно в type, но я такое не часто встречал). Примеры тут буду приводить только для функциональных компонентов, так как статья рассчитана на новичков, а они, как правило, пишут уже на функциональных. В примере ниже я использую деструктуризацию пропсов прямо в параметрах стрелочной функции:

const Component = ({ name, age, isActive }) => (...);

В некоторых проектах можете встретить такой подход:

const Component = (props) => {
  const { name, age, isActive } = props;
  return (...);
};

Кто-то использует props.name.

Способ обращения к пропсам не важен, созданный интерфейс нужно передать дженериком в тип FC объекта React - React.FC<ComponentProps>.

import React from 'react'; // с React 17 для создания компонента этот импорт не обязателен, но если мы используем React.FC - то нужно

interface ComponentProps {
  name: string;
  age: number;
  isActive?: boolean; // ? - необязательный пропс
}

const Component: React.FC<ComponentProps> = ({ name, age, isActive }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <p>Active: {isActive ? 'Yes' : 'No'}</p>
</div>
);

export default Component;

Есть популярное и общепринятое соглашение к названию таких интерфейсов добавлять постфикс "Props". Это название необходимо передать дженериком в метод FC объекта React.
Можно написать так: React.FC<ComponentProps>.

Можно импортировать сразу тип FC (FunctionComponent):

import { FC } from 'react';

interface ComponentProps {
  name: string;
  age: number;
}

const MyComponent: FC<ComponentProps> = ({ name, age }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

export default MyComponent;

Второй способ затипизировать props - не использовать FC тип, а указать в параметрах стрелочной функции - ({ name, age }: ComponentProps) => { return (...)}

interface ComponentProps {
  name: string;
  age: number;
}

const Component = ({ name, age }: ComponentProps) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

export default Component;

Разница такой типизации в особенности React.FC, которая по умолчанию тайком подкидывает нам типизацию children (типизирует его как React.ReactNode - об этом типе мы поговорим ниже). Это может быть удобным в некоторых случаях, но также может привести к путанице, если вы не планируете использовать children в своем компоненте или нужно уточнить его тип более конкретно. При использовании React.FC, TypeScript по умолчанию добавляет children в props вашего компонента. Это означает, что ваш компонент будет ожидать children, даже если вы их не используете.

Пример использования React.FC

import { FC } from 'react';

interface MyComponentProps {
  name: string;
  age: number;
}

const MyComponent: FC<MyComponentProps> = ({ name, age, children }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <div>{children}</div> {/* children автоматически типизированы */}
</div>
);

export default MyComponent;

Из-за такой особенности React.FC, при использовании этого синтаксиса добавления типизации пропсов, хорошей практикой является явно типизировать children, если они предполагаются:

import { FC, ReactElement } from 'react';

interface ComponentProps {
  name: string;
  age: number;
  children?: ReactElement; // Явно указываем тип для children
}

const MyComponent: FC<ComponentProps> = ({ name, age, children = null }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    {children} {/* children автоматически типизированы */}
  </div>
);

export default MyComponent;

Хорошей практикой также является присваивать значение по умолчанию необязательным пропсам, как показано в примере выше (children = null). Это помогает избежать ошибок, если пропс не был передан.

Типизация children

Теперь о типизации пропса children, то есть дочерних компонентов, которые передаются между JSX тегами, например: <Component>{children}</Component> или <Component><SomeComponent /></Component>. Существует два популярных способа их типизации, и каждый из них нужен для разных задач:

  1. Самый универсальный - React.ReactNode: Используйте его, если вам нужна гибкость при передаче в children, так как он охватывает все типы: строки, числа, булевы значения, фрагменты (массивы JSX элементов), null, undefined, а также ReactElement. Например, в компоненте модального окна можно передать как строку, так и компонент.

    import React, { ReactNode } from 'react';
    
    interface ModalProps {
      title: string;
      children?: ReactNode; // Универсальный тип для children
    }
    
    const Modal: React.FC<ModalProps> = ({ title, children }) => (
      <div className="modal">
        <h1>{title}</h1>
        <div>{children}</div>
      </div>
    );
    
    export default Modal;
  2. ReactElement: Используйте этот тип, когда вы разрешаете передавать только JSX (React) элементы и компоненты. Это более ограниченный тип по сравнению с React.ReactNode.

    import React, { ReactElement } from 'react';
    
    interface ButtonProps {
      label: string;
      children?: ReactElement; // Только React элементы
    }
    
    const Button: React.FC<ButtonProps> = ({ label, children }) => (
      <button>
        {label}
        {children}
      </button>
    );
    
    export default Button;

Примечание: JSX.Element также существует как тип, но на практике его редко используют, так как TypeScript и так неявно типизирует возвращаемое из компонента значение. Например:

const Component = (): JSX.Element => (
  <div>
    Hello, World!
  </div>
);

export default Component;

Заключение

Типизация props в React помогает создать более надежное и предсказуемое приложение. Использование PropTypes полезно в менее сложных проектах, тогда как TypeScript предлагает более мощные инструменты для работы с большими и сложными кодовыми базами. TypeScript не только упрощает рефактор кода, но и предоставляет богатый набор инструментов для работы с типами, что делает разработку, поддержку и рефакторинг кода более удобным и безопасным.

Благодарю за прочтение! Надеюсь ком то помог.
Буду рад любым комментариям и уточнениям с вашей стороны.
Удачи работяги!

Комментарии (9)


  1. serginho
    30.07.2024 20:43
    +1

    для children правильнее писать так:
    interface ComponentProps extends React.PropsWithChildren { ...


    1. 1andy
      30.07.2024 20:43
      +2

      скорее ‘type ComponentProps = PropsWithChildren<{…}>’


    1. danielzagumennyi
      30.07.2024 20:43
      +1

      Вообще-то PropsWithChildren принимает дженерик и верный вариант:

      PropsWithChildren<ComponentProps>


      1. Andrey098 Автор
        30.07.2024 20:43

        Тоже так использую)


  1. 1andy
    30.07.2024 20:43
    +6

    Надо заметить, что prop-types выделены в отдельный пакет начиная с React 15.5, потому что они «deprecated». А в React 19 они уже просто игнорируются.


    1. Andrey098 Автор
      30.07.2024 20:43

      Благодарю за уточнение!


  1. Alexandroppolus
    30.07.2024 20:43

    React.FC, которая по умолчанию тайком подкидывает нам типизацию children

    Это давно уже убрали, вроде с 17 версии.