Основы типизации props в React
Данная статья рассчитана на тех, кто только начинает писать свои 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 }) => (
Name: {name}
Age: {age}
Active: {isActive ? 'Yes' : 'No'}
);
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 }) => (...);
В некоторых проектах можете встретить такой подход:
typescriptКопировать кодconst Component = (props) => {
const { name, age, isActive } = props;
return (...);
};
Кто-то использует props.name
.
Способ обращения к пропсам не важен, созданный интерфейс нужно передать дженериком в тип FC объекта React — React.FC
.
import React from 'react'; // с React 17 для создания компонента этот импорт не обязателен, но если мы используем React.FC - то нужно
interface ComponentProps {
name: string;
age: number;
isActive?: boolean; // ? - необязательный пропс
}
const Component: React.FC = ({ name, age, isActive }) => (
Name: {name}
Age: {age}
Active: {isActive ? 'Yes' : 'No'}
);
export default Component;
Есть популярное и общепринятое соглашение к названию таких интерфейсов добавлять постфикс «Props». Это название необходимо передать дженериком в метод FC объекта React.
Можно написать так: React.FC
.
Можно импортировать сразу тип FC (FunctionComponent):
import { FC } from 'react';
interface ComponentProps {
name: string;
age: number;
}
const MyComponent: FC = ({ name, age }) => (
Name: {name}
Age: {age}
);
export default MyComponent;
Второй способ затипизировать props — не использовать FC тип, а указать в параметрах стрелочной функции — ({ name, age }: ComponentProps) => { return (...)}
interface ComponentProps {
name: string;
age: number;
}
const Component = ({ name, age }: ComponentProps) => (
Name: {name}
Age: {age}
);
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 = ({ name, age, children }) => (
Name: {name}
Age: {age}
{children} {/* children автоматически типизированы */}
);
export default MyComponent;
Из-за такой особенности React.FC
, при использовании этого синтаксиса добавления типизации пропсов, хорошей практикой является явно типизировать children
, если они предполагаются:
import { FC, ReactElement } from 'react';
interface ComponentProps {
name: string;
age: number;
children?: ReactElement; // Явно указываем тип для children
}
const MyComponent: FC = ({ name, age, children = null }) => (
Name: {name}
Age: {age}
{children} {/* children автоматически типизированы */}
);
export default MyComponent;
Хорошей практикой также является присваивать значение по умолчанию необязательным пропсам, как показано в примере выше (children = null
). Это помогает избежать ошибок, если пропс не был передан.
Типизация children
Теперь о типизации пропса children
, то есть дочерних компонентов, которые передаются между JSX тегами, например:
или
. Существует два популярных способа их типизации, и каждый из них нужен для разных задач:
Самый универсальный —
React.ReactNode
: Используйте его, если вам нужна гибкость при передаче вchildren
, так как он охватывает все типы: строки, числа, булевы значения, фрагменты (массивы JSX элементов),null
,undefined
, а такжеReactElement
. Например, в компоненте модального окна можно передать как строку, так и компонент.import React, { ReactNode } from 'react'; interface ModalProps { title: string; children?: ReactNode; // Универсальный тип для children } const Modal: React.FC
= ({ title, children }) => ( {title}
{children}ReactElement
: Используйте этот тип, когда вы разрешаете передавать только JSX (React) элементы и компоненты. Это более ограниченный тип по сравнению сReact.ReactNode
.import React, { ReactElement } from 'react'; interface ButtonProps { label: string; children?: ReactElement; // Только React элементы } const Button: React.FC
= ({ label, children }) => ( ); export default Button;
Примечание: JSX.Element
также существует как тип, но на практике его редко используют, так как TypeScript и так неявно типизирует возвращаемое из компонента значение. Например:
const Component = (): JSX.Element => (
Hello, World!
);
export default Component;
Заключение
Типизация props в React помогает создать более надежное и предсказуемое приложение. Использование PropTypes полезно в менее сложных проектах, тогда как TypeScript предлагает более мощные инструменты для работы с большими и сложными кодовыми базами. TypeScript не только упрощает рефактор кода, но и предоставляет богатый набор инструментов для работы с типами, что делает разработку, поддержку и рефакторинг кода более удобным и безопасным.
Благодарю за прочтение! Надеюсь ком то помог.
Буду рад любым комментариям и уточнениям с вашей стороны.
Удачи работяги!