Заключительная в цикле статья подводит итоги и рассказывает о том, как должна выглядеть типизация React-приложений по мнению автора. Также в конце статьи располагается опрос для получения обратной связи.

1. Как типизировать данные
2. Как понимать типы
3. Как использовать типизацию
4. Глубокая типизация
5. Связанная типизация
6. Изящная типизация

Три столпа типизации

Общий посыл цикла статей зиждется на трёх основах, следование которым сделает приложение правильно типизированным и использующим TypeScript с максимальной пользой:

  1. Естественное сужение типов.

  2. Глубокая типизация.

  3. Связанность типов.

Естественное сужение типов подразумевает добавление логики, при которой анализатор TypeScript может определить более точный тип. Глубокая типизация требует описывать тип исходных данных максимально подробно, чтобы не требовалась дополнительная логика сужения в месте их использования. Связанность типов позволяет формировать типы на основе существующих, что уменьшает код в контексте типов и объём работ с их поддержкой.

Типизация как документирование моделей данных

Нужно понимать, что документирование типа происходит не в тексте кода, а в совместной работе TypeScript и IDE. Поэтому контекст типов не должен быть засорён избыточным или дублирующим описанием типов. В следующем примере анализатор берёт тип дженерика из типа аргумента и явная передача типа в дженерик является избыточной.

const [current, setCurrent] = useState<string>('');

Код 6.1. Избыточное описание типа в дженерике

Если следовать этому подходу, то мы будем чаще проникать в то, что находится в типе, а не глядеть, то что написано в коде. Это позволит находить более компактные решения для типизации.

// контекст типов засорён избыточным типом null
const [data, setData] = useState<Data | null>(null);

// хотя можно использовать undefined, который уже заложен в типе useState
const [data, setData] = useState<Data>();
setData(undefined); // аргумент принимает тип Data | undefined

Код 6.2. Компактное решение для хранения в состоянии отсутствие объекта

Использование библиотек, противоречащих идеям типизации

Библиотека lodash идеально подходит для трансформации данных, если бы не отсутствие типизации.

import lodash from 'lodash';

const obj = {
  user: {
    id: 123
  }
}

const id = lodash.get(obj, 'user.id'); //тип any

Код 6.3. lodash мешает TypeScript определять тип переменной

Использование метода get, не гарантирует отсутствие ошибок. Вы можете сузить тип с помощью оператора as и ошибиться в записи второго аргумента. В случае изменения константы obj или исходного типа поля id, вам нужно самостоятельно определить, как менять логику её получения и обслужить тип в приведении через as. Если бы id получался прямым обращением к полю, то анализатор TypeScript подсветил бы место проблемы.

Другой проблемой некоторых библиотек является плохая связанность типов под капотом. С точки зрения потребителя кода трудно судить, является ли это проблема недочётом разработки или этот нюанс связан с особенностями работы функций под капотом:

import React from 'react';
import { Form } from 'react-final-form';

const FormComponent = () => (
  <Form    
    mutators={{
      customFn: (args: MyType, state, utils) => {...}
    }}
    render={(renderProps) => {
      const customFn = renderProps.form.mutators?.customFn; // тип (...args: any[]) => any | undefined
    }}
    // другие пропсы
  >
  </Form>
);

Код 6.4. react-final-form не связывает типы пропсов

Библиотека react-final-form связывает логикой переданные пропсы в компонент Form, но не связывает их типы. Таким образом разработчик обязан сужать типы самостоятельно.

Иногда подобные ситуации вынужденные, но при проектировании приложения следует учитывать абсурдность идеи совмещения паттернов антитипизации с TypeScript.

Определение базового типа

При связывании типов в приложении нужно разобраться от какого типа должен строиться другой. Иногда кажется, что самый большой по данным тип и является базовым, а все остальные очень удобно построятся из него. Это не всегда соответствует действительному удобству. Большой тип может намного удобнее собираться из маленьких. Тут вопрос к тому, какие данные являются первоисточником для определения типа и их влияние на него требует гибкости других.

В небольшом проекте были медальки (достижения) за действия пользователя. При проектировании приложения, тип, отвечающий за характеристики медалек, был независимым. Но при описывании зависимых от него данных, стало ясно, что он сам основан на другом типе, отвечающем за сбор статистики действий пользователя. Оказалось, что условия для получения медалек имеют почти такой же тип как сама статистика. Это привело к переосмыслению структуры моделей и упростило логику получения медалек пользователем.

// названия метрик для сбора статистики
type Metrics = 'action1' | 'action2' | 'action3';

//тип статистики действий по метрикам
type Statistics = Record<Metrics, number>;

// тип условий для получения медалек (похож на тип Statistics)
type Conditions = Omit<Statistics, 'action1'>

// тип описывающий имена достижений
type AchieveNames = 'junior' | 'middle' | 'senior';

// тип описывающий условия достижений для каждой медальки (вначале казался базовым)
type AchievementConfig = Record<AchieveNames, Condition>;

// тип описывающий, какими медальки получены пользователем
type UserAchievements = Record<AchieveNames, boolean>;

Код 6.5. Формирование исходных и зависимых типов

Создание типов удобно до написания логики. Это позволяет не хранить в голове следующий шаг проектирования кода, уменьшить путаницу и ошибки, а в результате ускорить разработку.

Ранее шла речь о потоках данных, и глядя на схему передачи и трансформации данных в приложении закономерно думать, что исходные типы находятся наверху, а производные внизу. В этом правиле целесообразно делать исключения.

Когда мы отправляем данные на бэк, то формируем модель, соответствующую контракту в АПИ. Таким образом тип данных, отправляемых на бэк, зависит от этого контракта, а не от данных присылаемых с бэка (даже если поля этих типов совпадают). Правильным будет записать независимый тип. Гибкость такого подхода кроется в том, что в случае изменения контракта, мы будем работать только с типами трансформации данных для бэкэнда.

Разные стадии типизации проекта

Следовать описанным подходам удобно при проектировании нового приложения. Многие проекты переходили на TypeScript постепенно и содержат легаси код. Наличие нетипизированного рабочего кода в целом не проблема. Проблема таких проектов в том, что разработчики по инерции пишут новый код, используя старые практики.

Приложение может содержать множество потоков данных, они могут начинать свой путь в момент приёма ответа с бэка или же быть сгенерированными в любом месте фронтовой части. Отдельные потоки данных могут быть типизированы, а другие нет. Если типизировать данные на верхнем уровне, то это не помешает нетипизированному коду, где эти данные принимаются. У нетипизированного кода по-умолчанию тип any самый широкий и может принять любой более узкий тип.

Если же типизировать данные нижнего уровня, а верхний оставить нетипизированным, то TypeScript может потребовать дополнительной логики для естественного сужения any к нужным типам. Писать логику в конечных функциях или простых компонентах не так сложно, но в любом случае эти изменения чреваты последствиями в виде ошибок. Плюсом будет если функции покрыты юнит-тестами.

При пересечении потоков, можно под типом оставлять только часть данных, а старую логику типизировать как any. Это не противоречит подходам, так как мы добавляем any нетипизированным данным.

type Props = {
  id: string;      // пропс, связанный с новой типизированной логикой
  data: any; // пропс, связанный с нетипизированной логикой
}

Код 6.6. Пример пересечения в коде нетипизированных и типизированных данных

Таким образом в частично типизированном проекте может присутствовать старый код с проверенной логикой и новый, в котором используются подходы с типизацией.

Книга на основе цикла статей

Изначальной целью автора было написание книги по TypeScript. Как упоминалось в первой статье, задача цикла статей - сбор обратной связи от заинтересованных людей. Для тех, кто ознакомился со статьями, ниже есть опрос-анкета по поводу написания книги. Варианты ответов противоположны друг другу и, заполняя их, читатели могут задать свой образ идеальной книги.

Спасибо за внимание! Желаю всем изящной типизации их кода!

Комментарии (0)