TypeScript 2.0 был недавно выпущен. В этой версии TypeScript есть несколько новых дополнений, которые отлично подходят для разработки Redux в React: сужение типов для объединений с тегами.

Давайте рассмотрим пример из стартового набора Rangle.io Redux, фантастического ресурса, который помог мне, когда я погрузился в разработку Redux.

Этот редьюсер, который реализует простой счетчик увеличения/уменьшения, написан для TypeScript 1.8:

See the code in the full post.

Этот пример выполняет свою работу, но у него есть обратная сторона: мы не получаем никакой помощи от TypeScript при написании нашего редуктора. И модель счетчика, и наши действия в этом примере в значительной степени динамически типизированы. Существуют решения для статической типизации моделей Immutable.js, поэтому мы сосредоточимся на работе с действием более безопасным способом.

Тип Сужение

Чтобы увидеть проблему, подумайте, хотим ли мы, чтобы наше действие содержало свойство by, позволяющее нашему счетчику увеличиваться и уменьшаться на разные величины. Затем мы хотели бы иметь возможность ссылаться на это в нашем обработчике:

See the code in the full post.

TypeScript знает, что у наших действий есть свойство типа string, но наш тип не допускает ничего другого. И нет хорошего способа добавить by к типу, так как by не является глобальным для всех действий — он имеет отношение только к увеличению и уменьшению. Раньше единственным реальным вариантом было сделать action типа any и не получать никакой проверки нашей обработки by.

В TypeScript 2.0 появилась возможность сужать типы на основе свойства с фиксированным количеством значений. Он специально разработан для обработки такого типа сценариев. В TypeScript 2.0 мы можем обновить наши определения, чтобы TypeScript понимал, что наш оператор switch «проверяет» форму действия в каждом случае, и предоставлял нам доступ к нашим дополнительным свойствам. Для этого требуется несколько простых дополнительных шагов.

1. Определите константы

Ранее я упустил из виду определение констант типа действия. Примеры в стартовом наборе, с которых я начал, представляют собой простые определения констант:

See the code in the full post.

Эти простые объявления констант имеют строку типа — просто недостаточно конкретную, чтобы разблокировать специальную обработку TypeScript 2.0. Нам нужно обновить определения констант следующим образом:

See the code in the full post.

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

Эти значения имеют более точную информацию о типе, чем оригинал. Первоначальные определения имели тип string, поэтому их использование ничего не сообщало компилятору о содержимом константы. Новые определения сообщают компилятору, какое конкретное значение здесь разрешено. Функция, которая принимает строку, может получить любую строку, но функция, которая принимает INCREMENT_COUNTER, может получить только значение, равное App/INCREMENT_COUNTER.

(Типы строковых литералов не новы для TypeScript 2.0, но их использование является ключом к включению новых функций языка… хотя я бы хотел, чтобы было лучшее сокращение.)

2. Определите типы действий

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

Каждый из этих типов указывает, какие свойства мы ожидаем иметь с действием данного типа. Обратите внимание, что здесь мы используем наши новые типы INCREMENT_COUNTER и т. д. IncrementCounterAction — это объект со свойством типа, которое должно быть равно App/INCREMENT_COUNTER, и свойством by, которое является числом. Важно отметить, что IncrementCounterAction является подтипом всех действий, которые имеют вид { type: string }. Это связано с тем, что наш тип INCREMENT_COUNTER задает конкретную строку, поэтому он является подтипом всех строк.

Эти детализированные определения действий — это то, что мы будем использовать для информирования TypeScript о типах параметров в наших редьюсерах, но сначала нам нужно разобраться с особым случаем.

3. Определите тип для «других действий»

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

1. Существуют дополнительные значения помимо наших явных случаев…
2. …не подразумевается, что эти другие случаи могут быть несовместимы по типу с объявленными нами действиями.

Я смог сделать это, создав специальный тип для представления «других действий».

Другие действия имеют тип в виде пустой строки, которая не пересекается ни с одним из наших других типов (то, что в конечном итоге становится важным свойством). Мы увидим, как это работает, когда мы свяжем все это вместе:

Определить настраиваемый тип действия

После импорта наших новых типов *Action мы можем объединить все это, определив тип для встречных действий и объявив его в counterReducer:

See the code in the full post.

Эта версия правильно выполняет проверку типов в TypeScript 2.0 и, что еще лучше, понимает, что action.by — это число в нашем редюсере. Теперь мы получаем поддержку IDE для доступа к свойствам редьюсера для любого случая, который мы хотим добавить в наш CounterReducer. Посмотрим на компоненты.

Во-первых, обратите внимание, что CounterAction объединяет типы, которые мы определили ранее. Это создает единый тип для случаев, которые мы хотим обрабатывать в CounterReducer. Поскольку каждый из наших типов Action имеет тип строкового литерала для своего свойства type, есть только несколько вариантов значения type в нашем действии, насколько это касается TypeScript, а именно, значения наших констант действия или пустая строка. .

Затем мы определили действие с типом CounterAction и присвоили ему значение по умолчанию OtherAction. Это прямо эквивалентно исходному примеру в начале этого поста с одним изменением: тип OtherAction более специфичен.

Поскольку TypeScript считает, что знает все возможные значения типа, и для каждого типа действия существует ровно одно определение, он продолжает сужать тип действия в каждом случае. Таким образом, в нашей ветке INCREMENT_COUNTER действие имеет тип IncrementCounterAction. Так что все хорошо и TypeScript понимает мир.

Тонкая ложь

Не так быстро! Здесь происходит одна странная вещь. На самом деле наша функция вызывается с большим количеством типов действий, чем мы объявили здесь. Каждое действие в Redux направляется через наш редуктор, но наш тип, кажется, исключает все это. Что дает?

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

Мы используем это свойство здесь, чтобы этот метод работал.

Наш тип OtherAction имеет более конкретное определение типа, чем мы на самом деле полагаемся. Мы сообщаем TypeScript, что это должна быть пустая строка, а затем продолжаем обрабатывать случай OtherAction в случае по умолчанию. Это очень важно. Наличие пустой строки в OtherAction — это то, что сообщает TypeScript, что у нас есть еще один случай для обработки после того, как мы перечислим все остальные, но он думает, что знает, что это за значение. Если бы мы использовали строку типа для типа OtherAction, TypeScript не смог бы предположить, что действие в нашей ветке INCREMENT_COUNTER не является OtherAction, тип которого имеет такое же значение. (Как-то подло!)

Вот почему важно не упрощать обработку случая OtherAction с помощью предложения case вместо значения по умолчанию. Обрабатывая его в нашем случае по умолчанию, мы гарантируем, что обработчик также может позаботиться о реальных, непустых значениях. Обработка OtherAction в предложении case прервется во время выполнения, когда, например. системные действия проходят через редюсер. Эта небольшая ложь о наших типах — это то, что позволяет нам получить поведение сужения типов, которого мы жаждем в мире Redux, который с самого начала был разработан, чтобы быть открытым и динамичным.

Дополнительная безопасность

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

Первоначально опубликовано на spin.atomicobject.com 27 сентября 2016 г.