Привет! Меня зовут Владимир Земсков, я ведущий разработчик, занимаюсь развитием фронтенд-части в low code платформе билайна. В статье расскажу, как мы решили отказаться от PropTypes в пользу TypeScript для автоматического извлечения типов пропсов React-компонентов.
Наши разработчики давно просили эту возможность, справедливо возмущаясь: «Зачем описывать типы дважды — в TypeScript и PropTypes?». Тем более, что аналогичный механизм уже работал в Storybook.
Статья будет полезна:
Разработчикам, которые хотят автоматизировать извлечение типов из React-компонентов.
Создателям low-code платформ, UI-китов или систем документации, которые ищут альтернативы ручному описанию пропсов.
Всем, кто интересуется TypeScript Compiler API и статическим анализом кода для решения подобных задач.
Если вы недовольны текущими решениями для организации библиотек компонентов или просто любите технические кейсы — добро пожаловать под кат!
Поиск решения: первый этап
Сначала я обратился к знакомому мне инструменту ts-morh. Раньше я уже использовал его для рефакторинга. Например, когда в общем компоненте поменялся публичный интерфейс, и нужно изменить поведение во всем проекте, или вы решили применить фича-флаг и убрать его использование.
Эта библиотека написана поверх API-компилятора TypeScript и позволяет чуть проще с ним взаимодействовать. На самом деле, все ее возможности можно реализовать при помощи самого компилятора.
Встал вопрос — как же мне осуществить навигацию по типам? Если для ast есть встроенные инструменты, то в навигации типов становится уже все на так легко. Отчасти это связано с тем, что система типов позволяет создавать очень разнообразные варианты. Например, запустить doom.
Для понимания работы с навигацией по типам очень помог этот gist. В целом, с помощью него удалось написать первый proof of concept, который подтверждал, что задача решаема и будет работать за приемлемое время.
Поиск решения: второй этап
Вторым этапом стоило понять, как существующие решения решают эту проблему. Признаться честно, до этой задачи я использовал TypeScript только для описания типов в своих проектах и особо не заботился о том, как он работает. У меня были общие представления о работе с ast, которые я получил во время написания плагинов к eslint. Хотелось получить больше представления о работе с компилятором TypeScript — лучше всего это делать в уже готовых проектах, поэтому я пошел на гитхаб и стал их искать.
Посмотрев несколько проектов, я решил, что самый для меня понятный — это Storybook. Думаю, многим фронтенд-разработчикам он знаком. Давайте попробуем разобраться, как же он получает информацию о типах.
Получить более общую информацию можно в их гитхабе. Мне же было интересно, как конкретно типы из TypeScript попадают непосредственно в интерфейс.
При сборке проекта в поле “__docgenInfo” записывается информация, полученная при помощи react-docgen-typescript и react-docgen. Когда я разрабатывал решение, в коде использовались обе эти библиотеки. А когда я пишу эту статью, при проверки оказалось, что используется только react-docgen-typescript.
В чем же принципиальная разница между двумя этими решениями?
React-docgen-typescript использует TypeScript-компилятор для определения типов, а react-docgen бабель. Конечно, за счет того, что react-docgen работает только в контексте одного файла, его скорость значительно превосходить react-docgen-typescript. Однако данные, которые можно получить из типа значительно меньше.
Я попытался использовать react-docgen-typescript для решения своей задачи, но мне не хватило возможности кастомизации — хотелось больше контроля над данными, которые получают из типов. А еще эта библиотека некорректно резолвила типы из нашего монорепозитория (спойлер: для решения этой проблемы использовался ProjectService).
Поиск решения: третий этап
Третий этап — это написание собственного решения. Я посмотрел на работы нескольких библиотек и решил, что стоит использовать TypeScript-compiler. В ts-morh оказалось слишком много возможностей для редактирования, сохранения кода, которые я использовать не собирался, а готовой библиотеки, которая удовлетворяла бы всем запросом, я не нашел.
К сожалению, документация к нему устарела и не поддерживается, хотя общее представление о работе дает. Надеюсь, в новой версии 7 будет получше. А пока пришлось опираться на код самого typescript. Все-таки удобно, что можно понять, что к чему в самом компиляторе, зная typescript. Также помог опыт анализа типов при помощи ts-morh. И, конечно же, код react-docgen-typescript помог понять, как получить тип непосредственно props в React-компоненте.
Итак, у меня есть функция createLibrary
, которая принимает React-компоненты и путь до вызова этой функции. Давайте попробуем без погружения в детали рассмотреть решение, которое получилось.
Для начала нам надо создать проект TypeScript. Делается он просто: находим файл конфига при помощи ts.findConfigFile
и создаем ProjectService:
const service = new tsserver.server.ProjectService({
host: system,
cancellationToken: { isCancellationRequested: (): boolean => false },
useSingleInferredProject: false,
useInferredProjectPerProjectRoot: false,
session: undefined,
logger,
canUseWatchEvents: true,
});
service.setCompilerOptionsForInferredProjects(
configFile.options as ts.server.protocol.InferredProjectCompilerOptions,
);
Далее нам потребуется ts.Program
const languageService = service.getLanguageService();
const program = languageService.getProgram();
Уже при помощи Program, мы может открыть наш файл с вызовом createLibrary
и получить тип компонентов, которые мы передаем.
Пройдемся по каждому и вытащим props. Тут я использовал похожий на react-docgen-typescript подход.
Дальше нам нужно разобрать тип. Тут и пригодился опыт использования ts-morh. Кстати, в самом ts-morh есть резолв различных примтивов — например, boolean. Большинство из них опирается на наличие флагов у типов. Самое главное при рекурсивном обходе ограничить глубину и ширину типов, иначе какой-нибудь HtmlDivElement может подвесить ваш процесс.
Теперь можно все собрать и получить результат, который ожидаешь. Конечно, решить все возможные варианты типов у меня не получилось. Да и для определения в нашей платформе нужного контрола они все и не требуется.
На случай, если тип не удается привести к нужному значения или хочется переопределить контрол в коде, я добавил специальный хелпер тип.
type OverrideFieldType<OriginalType, Field extends FieldTypeValues> =
OriginalType & { __discriminator?: { field: Field;} };
Так я смог отдать нашим разработчикам новый подход к описанию контролов при помощи typescript.
Ключевые моменты
Переход с PropTypes на TypeScript упрощает поддержку типов и устраняет необходимость дублирования описаний.
Исследование существующих решений (ts-morph, react-docgen, react-docgen-typescript) помогло понять их ограничения и выбрать оптимальный путь.
Собственная реализация на основе TypeScript Compiler API дала больше гибкости и контроля над обработкой типов.
Использование ProjectService и Language Service позволило корректно работать с монорепозиторием и сложными типами.
Хотя решение не покрывает все возможные случаи типов, оно демонстрирует, как можно эффективно использовать TypeScript для анализа компонентов и интеграции в существующие инструменты.
Если вы сталкивались с похожими задачами или знаете альтернативные подходы — буду рад обсудить в комментариях! ?
gsaw
Я в typescript и react без году неделя и статья звучит для меня как "абракадабра". У нас есть два проекта на Яве и мы все больше явисты, чем ui-сты У обоих проектов есть ui часть, созданная сначала для тупой визуализации данных, но разросшихся до средних таких размеров. Как водится выросли дикие заросли из компонентов, причем в обоих проектах дупликаты и по сути и по отображению. Вот и подумываю, как бы все эти компоненты вынести в библиотеку. И причесать структуру проектов.
И вот чувствую, что статья на важную тему, но совершенно непонятна для меня. Хотел вот и спросить, может есть статьи тут на эту тему, что бы почитать, по организации кода проекта, по структуре. Хотя бы правильные ключевые слова.
Спасибо
zemskovs Автор
Думаю, что для ui вашего приложения действительно не подходит эта статья. Это похоже на использование рефлексии в java. Конечно, есть нюансы, но для общего понимания надеюсь станет легче.
По организации кода не могу не порекомендовать статьи от коллег:
- https://habr.com/ru/companies/beeline_tech/articles/860612/
- https://habr.com/ru/companies/beeline_tech/articles/862558/
Также можете посмотреть в сторону FSD. А также доклад по практическому использованию fsd на holyJS