Пропсы и их типы

зачем React≈ 10 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В части II вы уже отделили данные от разметки: render() собирал строку из массива задач. Компонент делает то же самое, но данные приезжают в него аргументом и по дороге не превращаются в текст. А тип пропсов превращает опечатку в разметке в обычную диагностику компилятора — ту самую, что вы читали всю часть III.

Разделы темы:·

Компонент — обычная функция, и параметр у неё ровно один, сколько бы атрибутов вы ни написали в разметке. React собирает их в объект и передаёт целиком. Имя props — просто традиция, аргумент можно назвать как угодно.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два тега — два вызова функции, поэтому в выводе две строки. Внутри Row нет ни атрибутов, ни узлов DOM: есть объект с полями label и value — ровно с теми именами, что стоят в JSX.

Имена почти всегда достают прямо в сигнатуре — это обычная деструктуризация из части I. И вот что важнее синтаксиса: в шаблонной строке из части II любое значение становилось текстом, а в пропсах оно остаётся собой.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

value приехало числом, и его можно умножить прямо в разметке; done приехало булевым, и его можно проверить. В render() из части II для этого пришлось бы держать данные отдельно от строки разметки и не путать их — здесь компилятор следит за этим сам.

Атрибут в кавычках — строка, атрибут в фигурных скобках — выражение JavaScript. Отсюда value={92}, а не value="92".

Атрибут key выглядит как проп, но в объект пропсов не попадает: React забирает его себе, чтобы узнавать элементы между перерисовками. Прочитать его внутри компонента нельзя — если значение нужно и там, его передают вторым, обычным пропом.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пока у параметра нет типа, компилятор не знает о компоненте ничего — и под strict сразу говорит об этом той же диагностикой, что и в части III.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип пропсов объявляют рядом с компонентом и называют по его имени: PriceProps. Дальше начинается то, ради чего в React вообще берут TypeScript.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор указал на атрибут title. Но кадр под примером не пустой: React честно нарисовал «2024 — 990 дирхамов». Типы стираются при сборке — компилятор ругается, браузер выполняет получившийся JavaScript. Диагностика здесь не защита во время работы, а разговор с вами до запуска.

Ошибиться можно ещё двумя способами: забыть обязательный проп и передать лишний. Оба ловятся на месте использования.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пропущенный amount — это undefined, а undefined React просто не рисует: в кадре осталась дырка между тире и словом «дирхамов». Без типов такие дырки ищут глазами по готовой странице.

Что сделалиЧто скажет компилятор
Передали число в title: stringТип «number» не может быть назначен для типа «string».
Не передали amountСвойство «amount» отсутствует в типе «{ title: string; }» и является обязательным в типе «PriceProps».
Передали лишний colorСвойство «color» не существует в типе «IntrinsicAttributes & PriceProps».

IntrinsicAttributes в тексте ошибки — служебный тип React: в нём живёт то, что React забирает себе, вроде key.

Компонент можно писать парным тегом. Всё, что окажется внутри, React положит в проп с зарезервированным именем children — и объявить его в типе придётся вам.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В объекте пропсов два поля, хотя атрибут написан один. Тип React.ReactNode означает «то, что React умеет нарисовать»: строка, число, элемент, массив элементов, null. Он намеренно широкий — в него влезает и текст, и целое поддерево.

Забыть про children в типе — частая ошибка первых дней. Сам компонент при этом компилируется без единого замечания, ругань появляется там, где его используют.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ничего волшебного в парной записи нет: то же самое можно передать атрибутом. React разбирает содержимое тега и кладёт его в поле children — оба варианта дают одинаковый элемент.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Пишут, конечно, парным тегом: вложенную разметку так видно глазами. Но помнить, что это поле объекта, полезно — например, когда children нужно достать из пропсов и разложить по местам.

Знак вопроса делает проп необязательным: тип поля становится string | undefined, а компилятор перестаёт требовать атрибут. Значение по умолчанию задают там же, где достают имя, — в деструктуризации.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Третья строка вывода — пустые кавычки, а третий пункт списка пуст. Значение по умолчанию подставляется ровно тогда, когда пришло undefined: пустая строка, ноль и false — законные значения, и подменять их никто не станет. Это правило деструктуризации из части I, React ничего к нему не добавляет.

Если пустую строку тоже нужно считать «ничем», это делают явно — оператором || внутри компонента. Тогда решение видно в коде, а не прячется в сигнатуре.

А null в необязательный проп не пройдёт: «необязательный» — это про undefined, и strictNullChecks из части III держит эту границу.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Старый способ задавать умолчания — статическое поле Component.defaultProps — в настоящем проекте на функциональном компоненте не работает: разметку там переписывают в jsx из react/jsx-runtime, а тот этого поля не читает вовсе. Проверить это в песочнице не выйдет: здесь трансформ классический, и React.createElement поле пока подставляет — то есть приём «работает» ровно до переноса в проект. Умолчания живут в деструктуризации.

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

Часто компонент — тонкая обёртка над обычным тегом: своя пара пропсов плюс всё, что и так умеет <button>. Перечислять атрибуты кнопки руками бессмысленно, тип берут готовый — React.ComponentProps<"button">.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

...rest — остаточная деструктуризация из части I: туда попадает всё, что вы не разобрали по именам. Спред {...rest} в разметке раскладывает объект обратно в атрибуты, поэтому type, title и disabled доезжают до настоящей кнопки нетронутыми.

Заодно компилятор теперь знает про кнопку всё, что знает React, — и про ваш собственный проп тоже.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первая диагностика про tone — это литеральные типы из части III: «loud» не входит в объединение. Вторая ловит href, которого у кнопки нет, и даже предлагает замену — компилятор ищет похожее имя среди допустимых.

Проверка лишних свойств работает только для литерала, написанного на месте, — это раздел «Тот же объект, но литералом» статьи «Структурная типизация и проверка лишних свойств» из части III. Разложите тот же объект спредом — и проверка выключится: лишнее поле уедет в компонент, а компилятор не скажет ни слова.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ругань будет только на последней строке — там, где extra написан атрибутом. Отсюда практическое правило: {...данные} в разметке — место, где типы защищают слабее всего, и туда стоит подавать объект известного типа, а не собранный по дороге.

  • Компонент — функция с одним параметром: все атрибуты тега React собирает в объект пропсов.
  • В пропсы приезжают настоящие значения, а не текст: число остаётся числом, булево — булевым.
  • Тип пропсов объявляют рядом с компонентом; несовпадение показывается диагностикой компилятора, но выполнение она не отменяет — типы стираются при сборке.
  • children — обычный проп с зарезервированным именем: React кладёт туда содержимое парного тега, а объявить его в типе нужно самому.
  • Значение по умолчанию задают в деструктуризации, и подставляется оно только вместо undefined — пустая строка и ноль проходят как есть.
  • Остаток пропсов пробрасывают спредом, взяв тип у настоящего тега через React.ComponentProps; проверка лишних свойств при спреде не работает.

1/4Компонент объявлен с interface PriceProps { title: string; amount: number }, а в разметке написано <Price title={2024} amount={990} />. Что произойдёт при запуске примера?

2/4Что такое children в React?

3/4У компонента сигнатура function Label({ text = "нет данных" }: LabelProps). Что покажет <Label text="" />?

4/4В разметке написано <Row key="a" label="Первый" />. Какие ключи будут у объекта пропсов внутри Row?

Типизировать карточку

Компонент принимает заголовок, цену и признак скидки. Типа у пропсов нет, поэтому компилятор молчит про оба вызова — а они оба неверны: цена приехала строкой, а во втором теге её вообще нет. Объявите ProductProps, разберите пропсы деструктуризацией и почините вызовы.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Интерфейс называем по имени компонента и кладём рядом с ним. После этого компилятор сам показывает оба неверных вызова.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Строка "990" рисовалась не хуже числа — ровно поэтому её и нужно ловить типом: сломается она не в разметке, а на первом умножении. А sale без значения — это sale={true}, привычное сокращение из HTML.

Врезка с содержимым

Callout должен принимать любую вложенную разметку и необязательный заголовок со значением «Заметка» по умолчанию. Сейчас компилятор ругается на оба использования: children в типе не объявлен, и внутрь <aside> ничего не попадает.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Добавляем children в тип, достаём его в деструктуризации рядом с заголовком и ставим в разметку.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

title остаётся необязательным, children — обязательный: врезка без содержимого бессмысленна, и пусть об этом скажет компилятор, а не пустая рамка на странице.

Поле поверх настоящего input

Field должен принимать свой проп label и всё, что понимает <input>, — не перечисляя атрибуты руками. Сейчас компилятор ругается на оба вызова: в первом лишний placeholder, во втором — type. До disabled он не доходит: о лишнем свойстве компилятор сообщает по одному на тег.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Наследуем тип пропсов настоящего поля и пробрасываем остаток спредом.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Всё, что не разобрано по именам, оказывается в rest и уезжает на <input>. Если завтра понадобится maxLength или required, менять в компоненте нечего — они уже входят в React.ComponentProps<"input">.