---
# System prepended metadata

title: Как работать с типами в сервисе

---

# Как работать с типами в сервисе

## Типы

Типы в сервисе должны быть разделены на три группы:
**Удаленные типы | Основные типы | Типы представления**

Типы из этих групп не должны переиспользоваться между группами, даже если имеют одинаковую сигнатуру. Типы из каждой группы могут использоваться только для декларации данных, относящихся к этой группе (типы, описывающие свойства 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`. Для типов представления это обязательно;