Русский
Русский
English
Статистика
Реклама

Redux-thunk

Перевод Использование Redux в MV3 расширениях Chrome

23.12.2020 16:22:50 | Автор: admin

Примечание к переводу: Оригинальная статья была написана до того как стало известно о MV3. Тем не менее она полностью актуальна и для MV3 расширений (по крайней мере на данный момент). Поэтому я решил немного изменить ее название, добавив упоминание "MV3", что нисколько не противоречит содержанию. Если кто не в курсе: MV3 новый формат/стандарт расширений Chrome, должен быть введен в январе 2021 года.


В этой статье, предназначенной для опытных веб-разработчиков, рассматривается (и решается) проблема использования Redux в т.н. событийно-ориентированных (event-driven) расширениях Chrome.


Специфика событийно-ориентированных расширений


Событийно-ориентированная модель расширения впервые появилась в Chrome 22 в 2012 г. В этой модели фоновый скрипт расширения (если есть) загружается/выполняется только когда это нужно (в основном в ответ на события) и выгружается из памяти когда он ничего не делает.


Документация Chrome разработчика настоятельно советует использовать событийно-ориентированную модель для всех новых расширений, а для уже существующих расширений, использующих постоянную (persistent) модель делать миграцию. Есть правда одно исключение (в MV3 уже не актуально, в т.ч. поэтому переход на новую модель в MV3 обязателен). Но похоже что многие расширения до сих пор используют постоянную модель, даже если могут быть событийно-ориентированными. Конечно, многие из них были впервые выпущены до того, как стало известно о событийно-ориентированной модели. И теперь у их авторов просто нет стимула переходить на новую модель. С одной стороны это (пока) необязательно, а с другой означает необходимость множества изменений, не только в фоновом скрипте, но и в других компонентах расширения. К тому же многие при разработке используют кроссбраузерный подход, собирая готовые расширения для разных браузеров из одного и того же исходного кода. Событийно-ориентированная модель на данный момент поддерживается только в Chrome и она существенно отличается от постоянной модели, поддерживаемой остальными браузерами. Естественно, это усложняет кроссбраузерную разработку.


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


Проблема с Redux


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


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


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


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



Что касается событийно-ориентированной модели, то здесь в принципе не может быть такого сервера как при постоянном подходе, поскольку все компоненты расширения, включая фоновый скрипт, равны с точки зрения времени жизни. Другими словами, здесь нельзя хранить состояние в фоновом скрипте, т.к. последний может быть выгружен из памяти в любой момент (который нельзя узнать заранее). К счастью эта проблема также имеет решение.


Решение


Решение заключается в том, чтобы использовать chrome.storage как непосредственное место/способ хранения/изменения состояния. Этот подход (между прочим он явно предлагается в руководстве по миграции) предполагает, что состояние хранится непосредственно в chrome.storage, чье API вызывается всякий раз когда нужно изменить состояние, либо отследить такие изменения.



Как часть стандартного API расширений chrome.storage обладает рядом преимуществ, самое важное из которых то что любой компонент расширения может иметь прямой доступ к нему. Еще одно преимущество автоматическое сохранение состояния между сессиями браузера, что является встроенной фичей, работающей "из коробки".


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


Остается только одна проблема API chrome.storage отличается от Redux, что делает невозможным его использование в качестве замены Redux. Конечно, можно использовать chrome.storage как есть, либо написать к нему кастомную обертку. Однако, Redux уже успел стать чем-то вроде стандарта в управлении состоянием. Так что было бы неплохо как-нибудь адаптировать chrome.storage к принципам Redux, или другими словами, сделать Redux из chrome.storage).


Наша цель в этой статье получить Redux-совместимый интерфейс к chrome.storage, который будет переводить функционал chrome.storage в термины Redux. В терминах API нам нужно реализовать в рамках chrome.storage интерфейс функционала Redux, имеющего непосредственное отношение к хранилищу (store) Redux. Он включает в себя функцию createStore и возвращаемый ею объект Store (хранилище Redux). Ниже их интерфейсы:


Спецификация интерфейса

Реализация


Итак, прежде всего нам нужно написать класс, реализующий интерфейс Store. Назовем его ReduxedStorage.


Реализовать методы getState и subscribe достаточно просто, т.к. у них есть близкие аналоги в chrome.storage: метод get и событие onChanged. Конечно, они не могут напрямую заменить указанные методы Store, но могут помочь в организации хранения локальной копии состояния в нашем классе. Мы можем инициализировать локальное состояние в нашем классе, вызвав метод get из chrome.storage во время создания экземпляра ReduxedStorage и затем, всякий раз когда появляется событие onChanged, изменять соответственно локальное состояние. Таким образом гарантируется актуальность локального состояния. Тогда getState в рамках нашего класса будет тривиальным геттером. Реализация метода subscribe немного сложнее: он должен добавлять аргумент-функцию к некоторому массиву слушателей, которые будут вызываться всякий раз когда появляется событие onChanged.


В отличие от getState и subscribe, в chrome.storage нет ничего похожего на метод Store.dispatch. Там есть метод set, но его прямое использование противоречит еще одному фундаментальному принципу Redux, по которому состояние Redux присваивается только один раз, во время создания хранилища, после чего оно может быть изменено только через вызов метода dispatch. Так что нам нужно как-то воспроизвести функционал dispatch в нашем классе ReduxedStorage. Есть два способа сделать это. Радикальный предполагает полное воспроизведение соответствующего функционала Redux в рамках нашего класса, короче говоря, тупо скопировать код Redux. Но есть также и компромисный вариант, который и будет рассмотрен ниже.


Идея состоит в том, чтобы создавать новый экземпяр хранилища всякий раз, когда отправляется какое-то действие. Да, это звучит немного странно, но это единственная альтернатива полному копированию выше. Говоря более конкретно, всякий раз когда в нашем классе вызывается метод dispatch, нам нужно создать новый экземпяр хранилища, вызвав "оригинальную" функцию createStore, инициализовать его состояние локальным состоянием из нашего класса и наконец вызвать "оригинальный" метод Store.dispatch, передав ему аргументы из нашего dispatch. Помимо этого, к созданному хранилищу нужно добавить одноразовый слушатель изменения состояния, чтобы когда данное действие дойдет до хранилища, обновить chrome.storage новым состоянием, получающимся в результате данного действия. Далее это обновление должно быть отслежено и обработано слушателем события chrome.storage.onChanged, описанным выше.


Несколько замечаний насчет инициализации состояния: Поскольку метод chrome.storage:get выполняется асинхронно, мы не можем вызывать его из конструктора нашего класса. Поэтому нам придется перенести код вызова chrome.storage:get в отдельный метод, который должен вызываться сразу после конструктора (создания экземпляра класса). Этот метод, назовем его init, будет возвращать промис, который должен быть разрешен, когда метод chrome.storage:get завершит выполнение. В методе init нам также нужно создать еще одно локальное хранилище Redux, чтобы получить дефолтное состояние, которое будет использоваться, если состояние в chrome.storage в данный момент пусто.


Ниже пример как может выглядеть наш класс ReduxedStorage в первом приближении:


Реализация в первом приближении

Замечание: Мы обращаемся к части данных в chrome.storage под определенным ключом (this.key), чтобы иметь возможность сразу получить новое (измененное) состояние в слушателе chrome.storage.onChanged, не вызывая дополнительно метод chrome.storage:get. Кроме того, это может быть полезно при хранении в состоянии непосредственно массивов, т.к. chrome.storage позволяет хранить на корневом уровне только объект.


К сожалению, в реализации выше есть скрытый недостаток, который возникает из-за того, что мы обновляем свойство this.state не напрямую, а через метод chrome.storage:set, выполняющийся асинхронно. Само по себе это не проблема. Но при создании локального хранилища Redux внутри метода dispatch используется значение свойства this.state, что может представлять проблему, т.к. this.state не всегда может содержать актуальное состояние. Так может быть, если несколько действий отправляются синхронно сразу друг за другом. В этом случае 2-й и все последующие вызовы dispatch имеют дело с устаревшими данными в свойстве this.state, которое еще не успевает обновиться из-за асинхронного выполнения метода chrome.storage:set. Таким образом, синхронное отправление нескольких действий друг за другом может приводить к нежелательным результатам.


Чтобы решить указанную проблему, можно изменить код dispatch так, чтобы использовать для таких синхронных действий одно и то же хранилище Redux. Такое буферизированное хранилище должно быть сброшено по истечении небольшого периода времени (допустим 100 мсек), чтобы для следующих действий использовалось уже новое хранилище. Для этого решения нам потребуется добавить в наш класс дополнительные свойства для буферизированного хранилища и соответствующего состояния. Ниже пример как может выглядеть такая буферизированная версия метода dispatch:


Буферизированная версия dispatch

Как это часто бывает, решая одну проблему, можно получить другую. В нашем случае использование буферизированного многоразового варианта хранилища вместо локального одноразового может сломать асинхронную логику в Redux. Асинхронная логика, по умолчанию отсутствующая в Redux, может быть введена с помощью middleware, такого как Redux Thunk. С Redux Thunk можно например отложить отправку действия, используя создатель действия, который возвращает функцию вместо действия. Ниже пример такого создателя действия:


Пример отложенного создателя действия

delayAddTodo откладывает отправку действия 'ADD_TODO' на 1 сек.


Если мы попытаемся использовать такой создатель действия с буферизированным вариантом dispatch выше, мы получим ошибку во время вызова this.buffStore.getState внутри колбека this.buffStore.subscribe. Причина в том что колбек this.buffStore.subscribe вызывается как минимум через 1 сек после вызова нашего метода dispatch, когда this.buffStore уже сброшен в null (через 100 мсек после вызова dispatch). При этом предыдущий вариант dispatch без проблем работает с такими асинхронными создателями действий, т.к. использует локальное хранилище, которое всегда доступно соответствующему колбеку subscribe.


Таким образом, нам нужно совместить оба варианта, т.е. использовать, как буферизированный, так и локальный вариант хранилища Redux. Первый будет использоваться для синхронных действий, а последний для асинхронных, занимающих какое-то время, таких как delayAddTodo. Однако, это не значит, что нам нужны два отдельных экземпляра хранилища Redux в одном вызове dispatch. Можно создать экземпляр хранилища один раз, сначала сохранив его в свойстве this.buffStore, а затем скопировать ссылку на него в локальной переменной, назовем ее lastStore. Тогда, когда свойство this.buffStore будет сброшено, lastStore все еще будет указывать на тот же самый экземпляр хранилища и будет доступен соответствующему колбеку subscribe. Следовательно, внутри колбека subscribe можно использовать переменную lastStore как запасную ссылку на хранилище на тот случай, если свойство this.buffStore недоступно, что означает асинхронное действие "в действии"). Когда изменение состояния будет обработано внутренним колбеком subscribe, было бы полезно отписать данный колбек/слушатель от хранилища и сбросить переменную lastStore, чтобы высвободить соответствующие ресурсы.


Кроме того, было бы неплохо провести рефакторинг в коде класса, в т.ч.:


  • сделать свойства this.areaName, this.key изменяемыми/настраиваемыми через параметры конструктора.
  • переместить код, непосредственно вызывающий API chrome.storage, в отдельный класс, назовем его WrappedStorage.

Итак, ниже окончательная реализация нашего интерфейса:


Окончательная реализация

Его использование аналогично использованию оригинального Redux, за исключением того, что в нашем интерфейсе создатель хранилища (store creator) обернут в функцию и выполняется асинхронно, возвращая промис вместо созданного хранилища.


Стандартное использование интерфейса выглядит так:


Стандартное использование

Кроме того, с синтаксисом async/await, доступным начиная ES 2017, этот интерфейс может использоваться так:


Продвинутое использование

Исходный код доступен на Github.


Также этот интерфейс доступен как пакет в NPM:


npm install reduxed-chrome-storage
Подробнее..

Продвинутые дженерики в TypeScript. Доклад Яндекса

03.05.2021 12:05:25 | Автор: admin
Дженерики, или параметризованные типы, позволяют писать более гибкие функции и интерфейсы. Чтобы зайти дальше, чем параметризация одним типом, нужно понять лишь несколько общих принципов составления дженериков и TypeScript раскроется перед вами, как шкатулка с секретом. AlexandrNikolaichev объяснил, как не бояться вкладывать дженерики друг в друга и использовать автоматический вывод типов в ваших проектах.

Всем привет, меня зовут Александр Николаичев. Я работаю в Yandex.Cloud фронтенд-разработчиком, занимаюсь внутренней инфраструктурой Яндекса. Сегодня расскажу об очень полезной вещи, без которой сложно представить современное приложение, особенно большого масштаба. Это TypeScript, типизация, более узкая тема дженерики, и то, почему они нужны.

Сначала ответим на вопрос, почему TypeScript и при чем тут инфраструктура. У нас главное свойство инфраструктуры ее надежность. Как это можно обеспечить? В первую очередь можно тестировать.


У нас есть юнит- и интеграционные тесты. Тестирование нужная стандартная практика.

Еще нужно использовать ревью кода. В довесок сбор ошибок. Если все-таки ошибка случилась, то специальный механизм ее отсылает, и мы можем оперативно что-то пофиксить.

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

Синтаксис


Чтобы провести базовый ликбез, сначала рассмотрим основы синтаксиса.

Дженерик в TypeScript это тип, который зависит от другого типа.

У нас есть простой тип, Page. Мы его параметризуем неким параметром <T>, записывается через угловые скобки. И мы видим, что есть какие-то строки, числа, а вот <T> у нас вариативный.

Кроме интерфейсов и типов мы можем тот же синтаксис применять и для функций. То есть тот же параметр <T> пробрасывается в аргумент функции, и в ответе мы переиспользуем тот же самый интерфейс, туда его тоже пробросим.

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

Для классов существует похожий синтаксис. Прокидываем параметр в приватные поля, и у нас есть некий геттер. Но там мы тип не записываем. Почему? Потому что TypeScript умеет выводить тип. Это очень полезная его фишка, и мы ее применим.

Посмотрим, что происходит при использовании этого класса. Мы создаем инстанс, и вместо нашего параметра <T> передаем один из элементов перечисления. Создаем перечисление русский, английский язык. TypeScript понимает, что мы передали элемент из перечисления, и выводит тип lang.

Но посмотрим, как работает вывод типа. Если мы вместо элементов перечисления передадим константу из этого перечисления, то TypeScript понимает, что это не всё перечисление, не все его элементы. И будет уже конкретное значение типа, то есть lang en, английский язык.

Если мы передаем что-то другое, допустим, строку, то, казалось бы, она имеет то же самое значение, что и у перечисления. Но это уже строка, другой тип в TypeScript, и мы его получим. И если мы передаем строку как константу, то вместо строки и будет константа, строковый литерал, это не все строки. В нашем случае будет конкретная строка en.

Теперь посмотрим, как можно это расширить.

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

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

Посмотрим, как это происходит в функциях. Мы создаем функцию random. Она рандомно дает либо первый аргумент, либо второй.

Первый аргумент типа A, второй типа B. Соответственно, возвращается их объединение: либо тот, либо этот. В первую очередь мы можем явно типизировать функцию. Мы указываем, что A это строка, B число. TypeScript посмотрит, что мы явно указали, и выведет тип.

Но мы также можем воспользоваться и выводом типа. Главное знать, что выводится не просто тип, а минимально возможный тип для аргумента.

Предположим, мы передаем аргумент, строковый литерал, и он должен соответствовать типу A, а второй аргумент, единичка, типу B. Минимально возможные для строкового литерала и единички литерал A и та же самая единичка. Нам TypeScript это и выведет. Получается такое сужение типов.

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

Отношение типов


Типы можно условно рассматривать как некие множества. Посмотрим на диаграммку, где показан кусок всего множества типов.

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

Если идти в обратную сторону, то у каждого типа может быть подтип, меньше его.

Какие супертипы у строки? Любые объединения, которые включают строку. Строка с числом, строка с массивом чисел, с чем угодно. Подтипы это все строковые литералы: a, b, c, или ac, или ab.

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

И в этом порядке есть тип, как бы самый верхний, unknown. И самый нижний, аналог пустого множества, never. Never подтип любого типа. А unknown супертип любого типа.

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

Посмотрим, что нам даст знание этого порядка.

Мы можем ограничивать параметры их супертипами. Ключевое слово extends. Мы определим тип, дженерик, у которого будет всего один параметр. Но мы скажем, что он может быть только подтипом строки либо самой строкой. Числа мы передавать не сможем, это вызовет ошибку типа. Если мы явно типизируем функцию, то в параметрах можем указать только подтипы строки или строку apple и orange. Обе строки это объединение строковых литералов. Проверка прошла.


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

Посмотрим, как расширить эти ограничения.

Мы ограничились просто строкой. Но строка слишком простой тип. Хотелось бы работать с ключами объектов. Чтобы с ними работать, мы сначала поймем, как устроены сами ключи объектов и их типы.

У нас есть некий объектик. У него какие-то поля: строки, числа, булевы значения и ключи по именам. Чтобы получить ключи, используем ключевое слово keyof. Получаем объединение всех имен ключей.

Если мы хотим получить значения, то можем сделать это через синтаксис квадратных скобок. Это похоже на JS-синтаксис. Только он возвращает типы. Если мы передадим все подмножество ключей, то получим объединение вообще всех значений этого объекта.

Если мы хотим получить часть, то мы можем так и указать не все ключи, а какое-то подмножество. Мы ожидаемо получим лишь те поля, которым соответствуют указанным ключам. Если совсем всё редуцировать до одинарного случая, это одно поле, и один ключ дает одно значение. Так можно получать соответствующее поле.

Посмотрим, как использовать ключи объекта.

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

Посмотрим, как это работает с keyof. Мы определили тип CustomPick. На самом деле это почти полная копия библиотечного типа Pick из TypeScript. Что он делает?

У него есть два параметра. Второй это не просто какой-то параметр. Он должен быть ключами первого. Мы видим, что у нас он расширяет keyof от <T>. Значит, это должно быть какое-то подмножество ключей.

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

Смотрим на применение. У нас есть объект, в нем имена полей. Мы можем брать только их подмножество a, b или c, либо все сразу. Мы взяли a или c. Выводятся только соответствующие значения, но мы видим, что поле a стало обязательным, потому что мы, условно говоря, убрали знак вопроса. Мы определили такой тип, использовали его. Никто нам не мешает взять этот дженерик и засунуть его в еще один дженерик.

Как это происходит? Мы определили еще один тип Custom. Второй параметр расширяет не keyof, а результат применения дженерика, который мы привели справа. Как это работает, что мы вообще в него передаем?

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

Указанные аргументы не обязательно должны идти по порядку. Вроде как параметр P расширяет ключи T в дженерике CustomPick. Но никто нам не мешал указать его первым параметром, а T вторым. TypeScript не идет последовательно по параметрам. Он смотрит все параметры, что мы указали. Потом решает некую систему уравнений, и если он находит решение типов, которые удовлетворяют этой системе, то проверка типов прошла.

В связи с этим можно вывести такой забавный дженерик, у которого параметры расширяют ключи друг друга: a это ключи b, b ключи a. Казалось бы, как такое может быть, ключи ключей? Но мы знаем, что строки TypeScript это на самом деле строки JavaScript, а у JavaScript-строк есть свои методы. Соответственно, подойдет любое имя метода строки. Потому что имя у метода строки это тоже строка. И у нее оттуда есть свое имя.

Соответственно, мы можем получить такое ограничение, и система уравнений разрешится, если мы укажем нужные типы.

Посмотрим, как это можно использовать в реальности. Используем для API. Есть сайт, на котором деплоятся приложения Яндекса. Мы хотим вывести проект и сервис, который ему соответствует.

В примере я взял проект для запуска виртуальных машин qyp для разработчиков. Мы знаем, что у нас в бэкенде есть структура этого объекта, берем его из базы. Но помимо проекта есть и другие объекты: черновики, ресурсы. И у всех есть свои структуры.

Более того, мы хотим запрашивать не весь объект, а пару полей имя и имя сервиса. Такая возможность есть, бэкенд позволяет передавать пути и получить неполную структуру. Здесь описан DeepPartial. Мы его чуть попозже научимся конструировать. Но это значит, что передается не весь объект, а какая-то его часть.

Мы хотим написать некоторую функцию, которая бы запрашивала эти объекты. Напишем на JS. Но если присмотреться, видны опечатки. В типе какой-то Projeact, в путях тоже опечатка в сервисе. Нехорошо, ошибка будет в рантайме.

Вариант TS, казалось бы, не сильно отличается, кроме путей. Но мы покажем, что на самом деле в поле Type не может быть других значений, кроме тех, что есть у нас на бэкенде.

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

Посмотрим, как получить функцию.


У нас есть функция, ее использование. Есть вот эта структура. Сначала мы хотим получить все имена. Мы записываем такой тип, где имя соответствует структуре.

Допустим, для проекта мы где-то описываем его тип. В нашем проекте мы генерируем тайпинги из protobuf-файлов, которые доступны в общем репозитории. Далее мы смотрим, что у нас есть все используемые типы: Project, Draft, Resource.

Посмотрим на реализацию. Разберем по порядку.

Есть функция. Сначала смотрим, чем она параметризуется. Как раз этими уже ранее описанными именами. Посмотрим, что она возвращает. Она возвращает значения. Почему это так? Мы использовали синтаксис квадратных скобок. Но так как мы передаем в тип одну строку, объединение строковых литералов при использовании это всегда одна строка. Невозможно составить строку, которая одновременно была бы и проектом, и ресурсом. Она всегда одна, и значение тоже одно.

Обернем все в DeepPartial. Необязательный тип, необязательная структура. Самое интересное это параметры. Задаем их с помощью еще одного дженерика.

Тип, которым параметризуется дженерик параметров, также совпадает с ограничением на функцию. Он может принимать только тип имени Project, Resource, Draft. ID это, конечно, строка, она нам не интересна. Вот тип, который мы указали, один из трех. Интересно, как устроена функция для путей. Это еще один дженерик почему бы нам его не переиспользовать. На самом деле все, что он делает, просто создает функцию, которая возвращает массив из any, потому что в нашем объекте могут быть поля любых типов, мы не знаем каких. В такой реализации мы получаем контроль над типами.

Если кому-то это показалось простым, перейдем к управляющим конструкциям.

Управляющие конструкции


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

Что такое условные типы? Они очень напоминают тернарки в JavaScript, только для типов. У нас есть условие, что тип a это подтип b. Если это так, то возврати c. Если это не так возврати d. То есть это обычный if, только для типов.

Смотрим, как это работает. Мы определим тип CustomExclude, который по сути копирует библиотечный Exclude. Он просто выкидывает нужные нам элементы из объединения типов. Если a это подтип b, то возврати пустоту, иначе возврати a. Это странно, если посмотреть, почему это работает с объединениями.

Нам пригодится специальный закон, который говорит: если есть объединение и мы проверяем с помощью extends условия, то мы проверяем каждый элемент отдельно и потом снова их объединяем. Это такой транзитивный закон, только для условных типов.

Когда мы применяем CustomExclude, то смотрим поочередно на каждый элемент наблюдения. a расширяет a, a это подтип, но верни пустоту; b это подтип a? Нет верни b. c это тоже не подтип a, верни c. Потом мы объединяем то, что осталось, все плюсики, получаем b и c. Мы выкинули a и добились того, что хотели.

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

Как нам определить наш ранее упомянутый тип DeepPartial? Тут впервые используется рекурсия. Мы пробегаемся по всем ключам объекта и смотрим. Значение это объект? Если да, применяем рекурсивно. Если нет и это строка или число оставляем и все поля делаем опциональными. Это все-таки Partial-тип.

Этот рекурсивный вызов и условные типы на самом деле делают TypeScript полным по Тьюрингу. Но не спешите этому радоваться. Он надает вам по рукам, если вы попытаетесь провернуть нечто подобное, абстракцию с большой рекурсивностью.

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

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

Допустим, мы взяли ресурсы CPU, ядра. Всем нужны ядра. Я упростил пример, и тут всего лишь ресурсы, только ядра, и они представляют собой числа.

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

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

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

Посмотрим, как это реализовывается в TypeScript.

Мы определили функцию. У нее как раз три аргумента, сам объект, который мы патчим, и имя поля. Но это не просто имя поля. Оно может быть только именем числовых полей. Мы сейчас узнаем, как это делается. И сам патчер, который представляет собой чистую функцию.

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

Вновь разберем по порядку, как это происходит.

Мы пробегаемся по всем ключам переданного объекта, потом делаем вот такую процедуру. Смотрим, что поле объекта это подтип нужного, то есть числовое поле. Если да, то важно, что мы записываем не значение поля, а имя поля, а иначе вообще, пустоту, never.

Но потом получился такой странный объект. Все числовые поля стали иметь свои имена в качестве значений, а все не числовые поля пустоту. Дальше мы берем все значения этого странного объекта.

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

В примере показано: есть простой объект, поле единичка. Это число? Да. Поле число, это число? Да. Последняя строка не число. Получаем только нужные, числовые поля.

С этим разобрались. Самый сложный я оставил напоследок. Это вывод типа Infer. Захват типа в условной конструкции.


Он неотделим от предыдущей темы, потому что работает только с условной конструкцией.

Как это выглядит? Допустим, мы хотим знать элементы массива. Пришел некий тип массива, нам бы хотелось узнать конкретный элемент. Мы смотрим: нам пришел какой-то массив. Это подтип массива из переменной x. Если да верни этот x, элемент массива. Если нет верни пустоту.

В этом условии вторая ветка никогда не будет выполнена, потому что мы параметризовали тип любым массивом. Конечно, это будет массив чего-то, потому что массив из any не может не иметь элементов.

Если мы передаем массив строк, то нам ожидаемо возвратится строка. И важно понимать, что у нас определяется не просто тип. Из массива строк визуально понятно: там строки. А вот с кортежем все не так просто. Нам важно знать, что определяется минимально возможный супертип. Понятно, что все массивы как бы являются подтипами массива с any или с unknown. Нам это знание ничего не дает. Нам важно знать минимально возможное.

Предположим, мы передаем кортеж. На самом деле кортежи это тоже массивы, но как нам сказать, что за элементы у этого массива? Если есть кортеж из строки числа, то на самом деле это массив. Но элемент должен иметь один тип. А если там есть и строка, и число значит, будет объединение.

TypeScript это и выведет, и мы получим для такого примера именно объединение строки и числа.

Можно использовать не только захват в одном месте, но и сколько угодно переменных. Допустим, мы определим тип, который просто меняет элементы кортежа местами: первый со вторым. Мы захватываем первый элемент, второй и меняем их местами.

Но на самом деле не рекомендуется слишком с этим заигрываться. Обычно для 90% задач хватает захвата всего лишь одного типа.


Посмотрим пример. Задача: нужно показать в зависимости от состояния запроса либо хороший вариант, либо плохой. Тут представлены скриншоты из нашего сервиса для деплоймента приложений. Некая сущность, ReplicaSet. Если запрос с бэкенда вернул ошибку, надо ее отрисовать. При этом есть API для бэкенда. Посмотрим, при чем тут Infer.

Мы знаем, что используем, во-первых, redux, а, во-вторых, redux thunk. И нам надо преобразовать библиотечный thunk, чтобы получить такую возможность. У нас есть плохой путь и хороший.

И мы знаем, что хороший путь в extraReducers в redux toolkit выглядит так. Знаем, что есть PayLoad, и хотим вытащить кастомные типы, которые нам пришли с бэкенда, но не только, а плюс еще информация про хороший или плохой запрос: есть там ошибка или нет. Нам нужен дженерик для этого вывода.

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

Мы знаем, что хотим получить этот тип. Но у нас же не просто так появляется action. Нам нужно вызвать dispatch с этим action. И нам нужен вот такой вид, где по ключу запроса нужно отображать ошибку. То есть нужно поверх redux thunk примешивать такую дополнительную функциональность с помощью метода withRequestKey.

У нас, конечно, есть этот метод, но у нас есть и исходный метод API getReplicaSet. Он где-то записан и нам надо оверрайдить redux thunk с помощью некоего адаптера. Посмотрим, как это сделать.

Надо получить функцию вот такого вида. Это thunk с такой дополнительной функциональностью. Звучит страшно, но не пугайтесь, сейчас разберем по полочкам, чтобы было понятно.

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

Первое это просто наш API, объектик с методами. Мы можем делать getReplicaSet, получать проекты, ресурсы, неважно. Мы в текущем методе используем конкретный метод, а второй параметр это просто имя метода. Далее мы используем параметры функции, которую запрашиваем, используем библиотечный тип Parameters, это TypeScript-тип. И аналогично для ответа с бэкенда мы используем библиотечный тип ReturnType. Это для того, что вернула функция.

Дальше мы просто прокидываем свой кастомный вывод в AsyncThunk-тип, который нам предоставила библиотека. Но что это за вывод? Это еще один дженерик. На самом деле он выглядит просто. Мы сохраняем не только ответ с сервера, но и наши параметры, то, что мы передали. Просто чтобы в Reducer за ними следить. Дальше мы смотрим withRequestKey. Наш метод просто добавляет ключ. Что он возвращает? Тот же адаптер, потому что мы можем его переиспользовать. Мы вообще не обязаны писать withRequestKey. Это просто дополнительная функциональность. Она оборачивает и рекурсивно нам возвращает тот же самый адаптер, и мы прокидываем туда то же самое.

Наконец, посмотрим, как выводить в Reducer то, что нам этот thunk вернул.


У нас есть этот адаптер. Главное помнить, что там четыре параметра: API, метод API, параметры (вход) и выход. Нам надо получить выход. Но мы помним, что выход у нас кастомный: и ответ сервера, и параметр запроса.

Как это сделать с помощью Infer? Мы смотрим, что на вход подается этот адаптер, но он вообще любой: any, any, any, any. Мы должны вернуть этот тип, выглядит он вот так, ответ сервера и параметры запроса. И мы смотрим, на каком месте должен быть вход. На третьем. На это место мы и помещаем наш захват типа. Получаем вход. Аналогично, на четвертом месте стоит выход.

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

Так мы добились вывода типов, у нас есть доступ к ним уже в самом Reducer. В JavaScript сделать такое в принципе невозможно.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru