Данные в React-приложениях передаются по однонаправленному потоку: через пропсы (или React Context) от родительских компонентов к дочерним и через колбэки от дочерних к родительским. Типизация пронизывает этот поток, помогая разработчику документировать данные на разных слоях, обнаруживать ошибки на стадии написания кода и проектировать упрощённую логику.
При изменении логики на одном уровне приложения возможно появление ошибок в зависимом коде, которые анализатор TypeScript подсвечивает. Разработчику остаётся переписать типизацию и логику. Чтобы каждый раз не обслуживать зависимые типы, их можно конструировать на основе базовых, таким образом связывая типы.
Содержание
Связанность типов была продемонстрирована в прошлой статье на примере связи типа пропсов иконок и имен файлов, содержащих иконки. Большую часть проблем в случае изменения логики покрывает глубокая типизация. Связанная типизация демонстрирует в структуре типов, откуда данные приобретают свой тип, а также уменьшает повторяющийся код в контексте типов.
Рассмотрим обобщённый вид потока данных и их трансформацию в приложении React, использующее в качестве state-менеджера Redux.
Каждый слой в приложении выполняет свою задачу. Точки трансформации данных могут располагаться в других местах или вовсе отсутствовать. Это зависит от целесообразности и договорённостей на проекте.
Например, бэкэнд может присылать уже удобные данные, а преобразование данных для хранилища может происходить в редьюсере, а не миддлваре.
Авторское отступление
Хочется заметить, что бэковая и фронтовая части приложения - это две разных истории формирования данных. Бэкэнд-разработчики привязывают имена переменных и модели к сервисам и базе данных, фронтэнд-разработчики ориентируются на то, что видит пользователь. Поэтому пытаться прогнуть контракт под законы своего кода, немного неправильно.
Куда важнее подготовить фронтовое приложение к казусам несоответствия имён бэка и фронта, чтобы в случае чего с лёгкостью трансформировать нежелательные имена через готовое решение.
Мы можем увидеть, что данные, пришедшие на фронт и отправляемые на бэк, каким-то образом связаны. Для того, чтобы TypeScript контролировал эту связь, нужно связывать типизацию разных слоёв.
К примеру, данные с бэкэнда очень похожи на то, что мы хотим сохранить в хранилище. Тогда нужно создать тип, описывающий данные в store, на основе типа, описывающего модель данных с бэка:
type BackendData = {
profile_id: string | number;
// другие поля модели
};
type Id = BackendData['profile_id'];
type BackendDataWithoutId = Omit<BackendData, 'profile_id'>;
type StoreData = BackendDataWithoutId & {
id: Id
}
Код 5.1. Привязывание типа StoreData к типу BackendData с преобразованием поля profile_id
Для лучшего понимания кода были созданы промежуточные типы Id и BackendDataWithoutId, хотя можно обойтись и без них:
type StoreData = Omit<BackendData, 'profile_id'> & {
id: BackendData['profile_id']
}
Если в процессе создания новых фич в приложении, разработчики выясняют, что поле profile_id может иметь значение null, то достаточно будет это прописать в типе BackendData, связанные с ним типы автоматически подхватят новый тип и анализатор подсветит проблемы в логике на нижних слоях.
Точно так же мы могли бы описывать пропсы компонента, привязывая их типы к типу хранилища:
type Props = Pick<StoreData, 'id'> & {
// другие пропсы
}
Код 5.2. Привязывание типа Props к типам отдельных полей StoreData
Помимо того, что нам не требуется повторяться в коде, в очередной раз используя описанные типы, мы явно демонстрируем откуда эти типы происходят, что очень удобно в понимании общей структуры данных в приложении.
Таким образом через типы связываются все данные в приложении (если они связаны логикой).
В TypeScript многие операторы и конструкции используют базовый тип для построения нового:
keyof получает ключи из базового типа, удобен, когда нужно ориентироваться на ключи какого-то типа, вместо дублирования строк;
typeof определяет тип константы, как её видит анализатор, удобен вместо явного прописывания типа существующим в коде константным значениям;
mapped типы (с использованием оператора in) позволяет создавать объектные типы, используя ключи и их типы базового.
Pick, Omit, Partial и другие основаны на mapped типах и удобны для изменения исходного объектного типа.
Предпочтительно использовать связывание типов с их помощью, нежели переписывать новые типы, уменьшая гибкость типизации приложения.
Следующая статья: TypeScript в React-приложениях. 6. Изящная типизация