# Как работать с типами в сервисе ## Типы Типы в сервисе должны быть разделены на три группы: **Удаленные типы | Основные типы | Типы представления** Типы из этих групп не должны переиспользоваться между группами, даже если имеют одинаковую сигнатуру. Типы из каждой группы могут использоваться только для декларации данных, относящихся к этой группе (типы, описывающие свойства React-компонента, не должны использоваться в селекторах, коннекторах и т.д.). Не должно быть общих типов, описывающих поля c одинаковой сигнатурой у типов из разных групп; Допустимо использование основных типов в остальных группах в случаях, описанных далее, но основные типы ни в коем случае не должны использовать типы из удаленных типов и типов представления. ### Удаленные типы (RemoteTypes) Удаленные типы описывают ответы от эндпоинтов Api. Т.к. мы размещаем все методы работы с Апи в `src/common`, то и типы опишем там же. Так же разобъем типы по инстансам Api. #### Требования к данной группе типов: - Типы размещаются в директории `src/common/api/types/`; - Для каждого инстанса Api используется отдельный модуль вида `remote-<apiname>-api.ts` (например, `remote-simav3-api.ts`); - В модуле объявляется нэймспэйс вида `Remote<Apiname>API` (например `RemoteSimaV3API`); - В нэймспейсе объявляются и экспортируются интерфейсы, по одному на каждый эндпоинт данного инстанса Api. Имя интерфейса, по возможности, должно коррелировать с именем метода, который запрашивает данные с этим интерфейсом (например `fetchClaims` -> `Claims`). - Интерфейсы, описывающие вложенные структуры экспортируемых интерфейсов, создаются в этом же модуле, но не экспортируются (чтобы не засорять типы нэймспэйса при использовании); - Допускается использовать общий интерфейс для описания вложенных структур для разных эндпоинтов (экспортируемых интерфейсов), но только в пределах одного инстанса Api (нэймспейса). Потому что, даже если два инстанса Api предоставляют эндпоинты, в которых есть структуры данных, соответствующие одному интерфейсу, в случае изменения этого интерфейса в одном инстансе, никогда не произойдет одновременное аналогичное изменение в другом; - Из вышеуказанных пунктов следует, что в идеальном случае, в этих модулях (файлах) не должно быть импортов типов/интерфейсов из других ресурсов приложения; - Удаленные типы не должны, за редкими исключениями, импортироваться в других областях приложения (исключение - [адаптеры](#Адаптеры), либо декларация данных, возвращаемых из метода Api в сагах запроса данных, если нет другого технического решения); Например, шаги по добавлению типов для эндпоинта `/iapi/orders/v1/:id/desktop-main-view` (`iapi` это `ilium`): - Создаем модуль `/src/common/api/types/remote-ilium-api.ts`; - Объявляем и экспортируем нэймспейс с нужным интерфейсом: `/src/common/api/types/remote-ilium-api.ts`: ```typescript interface Order {} export namespace RemoteIliumAPI { export interface DesktopMainData { ... order: Order, ... } } ``` ⚠️ ***В дальнейшем, предполагается, что эти типы будут генерироваться на основании OpenAPI.*** Отдельно в этой группе можно выделить типы для декларации аргументов методов апи - данных, с которыми выполняется запрос к Api. Указанные типы можно объявить непосредственно в модуле api, либо, при их значительном количестве, вынести в модуль `/src/common/api/types/outgoing.ts`. #### Где использовать удаленные типы: - Модуль с реализацией методов Api; - Адаптеры Api; - В редких случая допустимо использование в сагах; ### Основные типы (CoreTypes) Основные или корневые типы - это типы, декларирующие сущности, используемые в приложении. Эти типы не должны зависеть от типов данных, предоставляемых апи и от реализации компонентов представления. Наоборот, остальные области приложения должны подстраиваться под эти типы. #### Требования к данной группе типов: - Модули с типами именуются в соответсвии с наименованием сущности, интерфейсы для которой они декларируют (например, `order.ts`, `claim.ts`, `manager.ts`); - Модули с типами размещаются в следующих директориях: - `src/common/types/` - `src/desktop/types/` - `src/mobile/types/` Соответственно, типы, использующиеся в обеих версиях, размещаются в `common`, типы, специфичные для конкретной версии приложения - в директории этой версии; - Если в десктопной и мобильной версии имеются отличные по сигнатуре интерфейсы для одной и той же сущности, но с одинаковыми интерфейсами для части вложенных структор, эти одинаковые интерфейсы необходимы выносить в общий модуль в директории `common`; - По возможности, следует избегать зависимостей между модулями с типами для разных сущностей приложения. Например, если в интерфейсе `Order` и интерфейсе `Product` есть поле `description` с одинаковой структурой `{ type: string, content: string }`, лучше не выносить интерфейс `Description` в какие то общие ресурсы, а объявить в каждом модуле отдельно; - Если для объявления типов сущностей создание отдельных модулей избыточно, в связи с малыми размерами интерфейсов, допустимо объявлять такие типы в индексном файле (`/index.ts`) соответствующей директории из списка выше; - Для объявления этих типов не должны использоваться типы из других групп приложения без исключений; - Следует избегать излишнего экспорта интерфейсов, если они используются для декларации полей из структуры других инерфейсов и более нигде не используются. Это позволит упростить импорт и автодополнение из-за уменьшения количества вариантов; - Допускается объявление типов по месту использования, вместо объявления в модулях в вышеуказанных директориях, если тип используется только в этом месте. Если тип потребовалось экспортировать, то это явный знак необходимости вынести его в модуль с типами; - Если возникает исключительная ситуация, при которой требуется создать модуль типов в директории, отличной от указанной выше, то следует сформировать имя модуля типов следующим образом: `<name>.types.ts`. Например, `super-duper.types.ts`; #### Где использовать основные типы: - Экшены; - Редьюсеры; - Саги; - Селекторы; - Коннекторы React-компонентов; - В редких случаях допустимо использование в React-компонентах в случаях использования компонента для отображения данных блочного Api (это апи изначально создается под нужды представления и не требует процессинга/адаптации внутри приложения); ### Типы представления (ViewTypes) Типы представления - интерфейсы, описывающие свойства React-компонентов. #### Требования к данной группе типов: - Типы объявляются в том же модуле, что и React-компонент; - Типы именуются в формате`I<ComponentName>Props`. Например, для компонента `Order` интерфейс будет называться `IOrderProps`. Это позволяет быстро отличить компонент от интерфейса при импорте и избавиться от ненужных переименований через `as` при именованном импорте; - Типы могут быть экспортированы для использования в родительских компонентах (например для декларации итемов в в интерфейсе компонента `OrderList`): ```typescript interface IOrderListProps { ... items: Array<IOrderProps> ... } ``` - Для определения внутренней структуры интерфейсов не могут использоваться типы из других областей приложения, даже если эти интерфейсы имеют одинаковую сигнатуру; - Типы представления не должны, за редкими исключениями, импортироваться в других областях приложения (например, коннекторах React-компонентов); #### Где использовать типы представления: - React-компоненты; - В редких случаях в коннекторе (для пререопределения типа библиотечного компонента через `type assertion` для возможности использования с дженериком свойств); ## Адаптеры #### Адаптеры удаленных типов Чтобы привести внешние данные из удаленного типа к основному типу, для дальнейшего использования, следует выполнить процессинг этих данных. Адаптер - это функция, выполняющая этот процессинг - преобразование данных от одного интерфейса к другому. <details> <summary><i>Как устроено сейчас</i></summary> В настоящий момент мы выполняем процессинг лишь для части сущностей, остальные же передаем в неизменном виде. И зачастую, при изменении интерфейса api (удаленного типа), мы начинаем менять для соответствия основной тип, что из-за сильной связанности основных типов создает лавинообразные изменения по всему коду приложения. Хотя, достаточно было бы использовать функцию-адаптер, которая выполнила бы преобразование данных нового удаленного типа к прежнему основному типу и позволила бы избежать этих изменений. </details> Для четкого проведения границы между удаленными и основными данными, предполагается наличие адаптеров во всех местах обращения к api, даже если удаленный тип и основной тип структурно идентичны и данные не требуют преобразования. В этом случае адаптер может представлять из себя просто функцию, [_.identity](https://lodash.com/docs/4.17.15#identity), но само ее наличие уже будет требовать соблюдения границ. Рекомендации по реализации: - Функция адаптер в большинстве случаев создается по месту использования - в саге запроса данных. Имя функции задается в виде одного из паттернов с использованием наименования обрабатываемой сущности: `<name>Adapter` или `process<Name>`, например `orderAdapter`, `proceesOrder`; - Функция-адаптер должна иметь типизированный аргумент и возвращаемое значение: принимает в качестве аргумента данные, соответствующие удаленному типу и возвращает данные, соответствующие основному типу; - Функция-адаптер должна выполнять преобразование данных после получения из метода api и перед записью данных в стор (в саге запроса данных из api); - Если в двух модулях требуется одинаковая функциональность адаптера (например, преобразование полей объекта из snake_case в camelCase), то не стоит использовать общую функцию для этого, т.к. это следает код излишне связанным, если потребуется расширение функциональности адаптера в одном из мест использования; - Если удаленный тип соответствует сигнатуре основного типа, то все равно требуется создание адаптера. В качестве адаптера может использоваться [_.identity](https://lodash.com/docs/4.17.15#identity), или даже [_.pick](https://lodash.com/docs/4.17.15#pick), чтобы наверняка отсечь неиспользуемые поля данных. Т.к. это функции дженерики, то можно передать в них ожидаемый тип; #### Адаптеры типов представления Границей, на которой происходит разделение основной части приложения и представления является коннектор React-компонентов. В большинстве случаев, React-компоненты представления должны разрабатываться так, чтобы реализовывать интерфейсы, соответствующие передаваемым данным из основной части приложения. Но возможны исключительные ситуации, при которых изменения представления нежелательны или невозможны: - В коннектор передан библиотечный компонент, интерфейс которого неизменен; - Компоненты представления были реализованы намного раньше основной части (чего в идеале быть не должно), и внесение изменений в них очень дорогостояще; В такой ситуации можно провести преобразование данных к типу представления в селекторах или непосредственно в коннекторах, если это не сильно усложняет его реализацию и тестирование. В этой ситуации не надо импортировать типы представления в селекторы/коннекторы - типы должны быть **выведены ts автоматически!** ## Рекомендации - Использовать `any` только в исключительных случаях. При чтении из `any` всегда реализовывать рантайм проверки значаний. Где есть возможность использовать `unknown` вместо `any` - используем `unknown`; - Не перебарщивать с типизацией. Иногда достаточно декларировать сущности как примитивы (`string`, `number`) без сужения типа до конкретных значений. Основная цель - обезапасить рантайм статической типизацией, а не реализовать валидацию значений данных (которой все равно не будет в рантайме); - Не утруждаться типизацией объектов, к свойствам которых не происходит доступа. Например - не надо описывать сложную структуру какого то поля из апи, если нам интересно только его наличие и мы не читаем из его свойств. Достаточно задать его как `unknown`; - Не перебарщивать с декларацией типов, где в этом нет необходимости. Если ts может сам вывести коректный тип - пусть выводит (пример - селекторы стэйта); - Не злоупотреблять `type assertion` и использовать с осторожностью - иногда можно переопределить тип данных, но забыть, что он может принимать nullish значение и словить ошибку в рантайме. В 80% случаев, если потребовалось его использование, то скорее всего что-то сделано не так самим разработчиком. В тестах же наоборот, использование приветствуется, чтобы не создавать сложные структуры неиспользуемых тестовых данных только для соответствия принимаемому типу; - Проверять наличие объявления нужных типов в приложении и выполнять непрерывный рефакторинг типов. Если вы планируете добавить какой то интерфейс в мобильной версии приложения, сначала проверьте, может быть он уже объявлен в десктопной версии и достаточно вынести его в общие сущности. Если же какой то интерфейс из общих сущностей перестал удовлетваорять обем версиям и вы решаете создать новый интерфейс, например, в десктопной версии, не забудьте убрать прежний интерфейс из общих типов в мобильный - в общих он больше не нужен; - Отдавать предпочтение использованию интерфейсов, а не псевдонимам типов. Использовать псевдонимы типов там, где нет возможности использовать интерфейс; - Для отделения наименования самой сущности от наименования ее типа в имя интерфейса можно начинать с префикса `I`. Например, `IOrder`. Для типов представления это обязательно;