
Иногда инженерные усовершенствования начинаются не с грандиозного плана, а с проявления любопытства к деталям.
Однажды, работая над React-компонентом, я заметил нечто странное.
Когда я напрямую указывал пропсы компонента, TypeScript сигнализировал о неиспользуемом пропсе. Но когда я оборачивал тот же компонент в React.FC, предупреждение исчезало.
Мне показалось это неправильным. TypeScript должен защищать нас от ошибок, а не просто игнорировать их.
Я открыл TypeScript Playground и провел несколько экспериментов. Действительно, React.FC изменял способ обработки пропсов компилятором. Создавалось впечатление, будто TypeScript вежливо говорил: «Не волнуйся, все в порядке», даже когда все было не так.
Это небольшое открытие положило начало пути, который в итоге затронул тысячи файлов и изменил наш подход к написанию компонентов во всем фронтенде Gusto.
Привычный паттерн со скрытыми издержками
В Gusto многие наши React-компоненты следовали привычному паттерну:
type ButtonProps = {
label: string
onClick?: () => void
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
)
Несколько лет назад это соглашение имело смысл. React.FC выглядел чисто и удобно и широко рекомендовался в ранних руководствах по TypeScript и React.
Но когда я начал копать глубже, стали проявляться изъяны.
React.FC выполняет несколько неочевидных действий, которые могут незаметно вводить разработчиков в заблуждение:
- позволяет передавать недопустимые значения пропсов по умолчанию без предупреждения об ошибках;
- скрывает неиспользуемые пропсы, поэтому TypeScript не предупреждает о мертвом коде;
- нарушает вывод типов для дженериков, что усложняет типизацию переиспользуемых компонентов.
По отдельности эти проблемы едва заметны. Вместе же они превращаются в технический долг, которого TypeScript как раз и должен помогать избегать.
Таким образом, мы незаметно накапливали технический долг.
Момент истины
Проблема стала очевидной, когда я сравнил две версии одного и того же компонента — одну с типизацией через React.FC, другую без:
// С React.FC — без предупреждения
const Example: React.FC<{ used: string; unused: string }> = ({ used }) => {
return <div>{used}</div>
}
// Без React.FC — корректное предупреждение
const Example = ({ used }: { used: string; unused: string }): React.ReactNode => {
return <div>{used}</div>
}
В первом случае TypeScript не предупреждал о том, что неиспользуемая переменная не используется. Во втором он делал именно то, что мы от него ожидаем.
В этот момент мы поняли: наша система типов настолько же хороша, насколько хороши типы, которым мы решили доверять. В данном случае React.FC давал обещания, которые не мог сдержать.
От обсуждений к изменениям во всей кодовой базе
Я поделился своим открытием в чате разработчиков. Сначала это был просто пост в духе «сегодня я узнал» — такой, который может собрать пару реакций в виде эмодзи и исчезнуть. Но этот пост вызвал настоящую дискуссию. Другие инженеры подключились, делясь похожими разочарованиями и припоминая подобные странности TypeScript.
Сразу стало ясно: мы не одиноки — и действительно можем с этим что-то сделать.
Здесь стоит сделать отступление и рассказать о культуре инженерии Gusto, потому что она сыграла ключевую роль в том, что произошло дальше. В Gusto реализация любой идеи редко воспринимается как результат индивидуальной работы. Сотрудничество — не просто абстрактная ценность; оно подкреплено конкретными структурами, которые облегчают выдвижение идей и получение содержательной обратной связи.
У нас есть гильдии, протоколы архитектурных решений (ADR), ревью технических спецификаций, график рабочих совещаний и открытые Slack-каналы, которые создают четкие пути для обсуждения. Если вы хотите поднять вопрос, касающийся фронтенда, можете прийти на совещание веб-команды. Для обсуждения изменений, затрагивающих общую инфраструктуру или соглашения, существуют устоявшиеся каналы и форумы, где такие обсуждения проходят естественным путем. А если идея требует более широкого согласования, она может быть оформлена через ADR и открыто рассмотрена.
Эта среда сыграла решающую роль. То, что начиналось как случайное наблюдение, не застряло на этапе «интересно, но рискованно». Напротив, оно быстро нашло соавторов, скептиков и сторонников — людей, которые помогли проверить идею на прочность и превратить ее в нечто осуществимое.
В течение нескольких дней мы решили выяснить, насколько масштабна проблема. И убедились: очень масштабна. Наш главный фронтенд-репозиторий содержал тысячи компонентов, типизированных с помощью React.FC. Почти каждый пакет должен был подвергнуться исправлениям.
Это было амбициозно — возможно, даже немного безрассудно. Но у нас были инструменты, поддержка и общее понимание, что результат — настоящая, обеспечиваемая безопасность типов — того стоил.
Автоматизация изменений
Шаг 1: Переписывание объявлений компонентов
Мы написали скрипт трансформации, который просканировал всю кодовую базу на предмет компонентов, определенных через React.FC. Для каждого из них он выполнял следующее:
- извлекал тип пропсов из React.FC<Props>;
- встраивал этот тип непосредственно в параметры компонента;
- добавлял явный возвращаемый тип (React.ReactNode).
// До
const Card: React.FC<CardProps> = ({ title }) => <div>{title}</div>
// После
const Card = ({ title }: CardProps): React.ReactNode => <div>{title}</div>
Результат стал проще, строже и понятнее для TypeScript.
Шаг 2: Обеспечение явных возвращаемых типов
Затем мы гарантировали, что каждый экспортируемый компонент явно объявляет свой возвращаемый тип. Если компонент возвращал JSX, но не указывал тип, мы автоматически добавляли его:
// До
export function Modal({ isOpen }: ModalProps) {
return isOpen ? <div>Open</div> : null
}
// После
export function Modal({ isOpen }: ModalProps): React.ReactNode {
return isOpen ? <div>Open</div> : null
}
Это дало каждому компоненту четкий контракт — и TypeScript мог его обеспечивать.
Волновой эффект от чистки
Как только миграция была запущена, произошло нечто неожиданное: компилятор начал выявлять ошибки, о существовании которых мы не подозревали:
- неиспользуемые пропсы, пережившие множество рефакторингов;
- значения по умолчанию с недопустимыми типами;
- несогласованные определения компонентов во всех проектах.
Убрав одну абстракцию, мы обнаружили десятки мелких проблем, которые оставались незамеченными. То, что начиналось как чистка типов, превратилось в значимое улучшение общего состояния кода.
Как мы сохранили этот результат
После миграции нам нужно было убедиться, что React.FC не вернется. Поэтому мы добавили правило для линтера, чтобы полностью заблокировать его:
"@typescript-eslint/no-restricted-types": [
"error",
{
"types": {
"FC": {
"message": "Avoid React.FC. Use explicit prop types and return values instead."
}
}
}
]
Теперь это правило автоматически обеспечивает соблюдение нового соглашения.
Чего мы добились
Результаты нашей работы к завершению проекта:
- обновлено более 5000 файлов;
- стандартизированы десятки фронтенд-пакетов;
- исправлены десятки скрытых ошибок;
- повышена предсказуемость и надежность системы типов.
Но настоящая победа была не только технической. Она была и культурной. Этот проект напомнил нам, что инженерное мастерство часто проистекает из внимательности — из умения замечать то, что кажется неправильным, и не успокаиваться, пока не докопаешься до сути проблемы.
Заключение
Отказ от React.FC был не проявлением педантизма — это было стремление к непогрешимости. Выбрав явную, предсказуемую типизацию вместо сомнительного удобства, мы получили более надежные гарантии и более чистую основу для будущего.
Иногда прогресс заключается не в принятии самого нового инструмента, а в пересмотре тех, которые мы безоговорочно приняли.
Читайте также:
- Как сократить ошибки в базе кода React
- Представляем SafeTest: новый подход к тестированию фронтенда
- Frontend Masters: принципы SOLID в React/React Native
Читайте нас в Telegram, VK и Дзен
Перевод статьи Arda Örkin: The Journey to a Safer Frontend: Why We Removed React.FC





