Обобщенные или параметризованные типы позволяют писать более гибкие функции и интерфейсы.
Чтобы пойти дальше, чем параметризация с помощью одного типа, вам нужно всего лишь понять несколько общих принципов написания дженериков — и TypeScript откроется перед вами, как ящик с секретами.
АлександрНиколаичев объяснил, как не бояться вкладывать дженерики друг в друга и использовать автоматический вывод типов в своих проектах.
— Всем привет, меня зовут Александр Николаичев.
Я работаю в Яндекс.
Облаке фронтенд-разработчиком, занимаюсь внутренней инфраструктурой Яндекса.
Сегодня я расскажу вам об очень полезной вещи, без которой сложно представить современное приложение, особенно большое.
Это TypeScript, типизация, более узкая тема — дженерики, и зачем они нужны.
Для начала ответим на вопрос, почему TypeScript и причем здесь инфраструктура? Наше главное свойство инфраструктуры – ее надежность.
Как это можно обеспечить? Прежде всего, вы можете протестировать.
У нас есть модульные и интеграционные тесты.
Тестирование является необходимой стандартной практикой.
Вам также необходимо использовать проверку кода.
Дополнительно — сбор ошибок.
Если ошибка все же возникает, то ее отправляет специальный механизм, и мы можем быстро что-то исправить.
Было бы здорово вообще не совершать ошибок.
Для этого есть типизация, которая вообще не позволит нам получить ошибку во время выполнения.
Яндекс использует отраслевой стандарт TypeScript. А поскольку приложения большие и сложные, то мы получим такую формулу: если у нас есть фронтенд, типизация и даже сложные абстракции, то мы обязательно придем к дженерикам TypeScript. Без них невозможно.
Синтаксис
Чтобы провести базовый ликбез, давайте сначала рассмотрим основы синтаксиса.Обобщенный тип в TypeScript — это тип, который зависит от другого типа.
У нас есть простой тип Page. Параметризуем его определенным параметром , записанное через угловые скобки.
И мы видим, что есть какие-то строки, числа, но наши является переменной.
Помимо интерфейсов и типов, мы можем использовать тот же синтаксис для функций.
То есть то же самое параметр передается в аргумент функции, и в ответе мы повторно используем тот же интерфейс и передаем его туда же.
Наш вызов дженерика также записывается через угловые скобки с нужным типом, как и при его инициализации.
Для классов существует аналогичный синтаксис.
Мы передаем параметр в частные поля, и у нас есть геттер.
Но мы не записываем туда тип.
Почему? Потому что TypeScript может определять тип.
Это очень полезная его функция, и мы ею воспользуемся.
Давайте посмотрим, что происходит, когда мы используем этот класс.
Мы создаем экземпляр, и вместо нашего параметром мы передаем один из элементов перечисления.
Создаем перечисление – русский, английский.
TypeScript понимает, что мы передали элемент из перечисления, и определяет тип lang.
Но давайте посмотрим, как работает вывод типа.
Если вместо элементов перечисления мы передаем константу из этого перечисления, то TypeScript понимает, что это не всё перечисление, не все его элементы.
И будет конкретное значение типа, то есть lang en, английский.
Если мы передадим что-то еще, скажем, строку, то она будет иметь тот же смысл, что и перечисление.
Но это уже строка, другой тип в TypeScript, и мы ее получим.
А если мы передаем строку как константу, то вместо строки будет константа, строковый литерал, это не все строки.
В нашем случае это будет конкретная строка en. Теперь давайте посмотрим, как мы можем это расширить.
У нас был один параметр.
Ничто не мешает нам использовать несколько параметров.
Все они пишутся через запятую.
В таких же угловых скобках и применяем их по порядку – от первой к третьей.
Подставляем нужные значения при вызове.
Скажем, объединение числовых литералов, определенного стандартного типа, объединение строковых литералов.
Они все просто записаны по порядку.
Давайте посмотрим, как это происходит в функциях.
Создаем случайную функцию.
Он случайным образом дает либо первый аргумент, либо второй.
Первый аргумент — типа А, второй — типа Б.
Соответственно, возвращается их объединение: либо то, либо это.
Прежде всего, мы можем явно ввести функцию.
Указываем, что A — строка, B — число.
TypeScript проверит то, что мы явно указали, и выведет тип.
Но мы также можем использовать вывод типа.
Главное — знать, что выводится не просто тип, а минимально возможный тип аргумента.
Предположим, мы передаем аргумент, строковый литерал, и он должен быть типа A, а второй аргумент, единица, должен быть типа B. Минимально возможным для строкового литерала и единицы является литерал A и тот же самый .
TypeScript выведет нам это.
Это приводит к сужению типов.
Прежде чем перейти к следующим примерам, мы рассмотрим, как типы вообще связаны друг с другом, как использовать эти отношения и как добиться порядка из хаоса всех типов.
Тип отношения
Типы условно можно рассматривать как определенные множества.Давайте посмотрим на диаграмму, на которой изображен фрагмент всего набора типов.
Мы видим, что типы в нем связаны определенными отношениями.
Но какие? Это отношения частичного порядка — что означает, что для типа всегда указан супертип, то есть тип «над ним», который охватывает все возможные значения.
Если пойти в обратном направлении, то у каждого типа может быть подтип, который «меньше» его.
Каковы супертипы строки? Любые соединения, включающие строку.
Строка с числом, строка с массивом чисел, с чем угодно.
Подтипы — это все строковые литералы: a, b, c, ac или ab. Но важно понимать, что порядок не линейный.
То есть не все типы можно сравнивать.
Это логично, и именно это приводит к ошибкам несоответствия типов.
То есть строку нельзя просто сравнить с числом.
И в этом порядке есть тип, который кажется верхним – неизвестный.
А нижний, аналог пустого множества, никогда.
Никогда не является подтипом любого типа.
А неизвестное — это супертип любого типа.
И, конечно, есть исключение – любое.
Это особый тип, он вообще игнорирует этот порядок и используется, если мы переходим с JavaScript, чтобы не заботиться о типах.
Не рекомендуется использовать что-либо с нуля.
Это стоит сделать, если нас не особо волнует положение типа в этом порядке.
Посмотрим, что нам дадут знания этого порядка.
Мы можем ограничить параметры их супертипами.
Ключевое слово расширяется.
Мы определим тип, обобщенный, который будет иметь только один параметр.
Но мы скажем, что это может быть только подтип строки или сама строка.
Мы не сможем передавать номера; это приведет к ошибке типа.
Если мы явно прописываем функцию, то в параметрах мы можем указать только подтипы строки или самой строки — яблоко и апельсин.
Обе строки представляют собой конкатенацию строковых литералов.
Тест пройден.
Мы также можем автоматически выводить типы на основе аргументов.
Если мы передали буквальную строку, то это тоже строка.
Проверка сработала.
Давайте посмотрим, как расширить эти ограничения.
Мы ограничились одной линией.
Но строка — слишком простой тип.
Я хотел бы работать с ключами объекта.
Чтобы работать с ними, сначала разберемся, как структурированы сами ключи объекта и их типы.
У нас есть некий объект. В нем есть несколько полей: строки, числа, логические значения и ключи по имени.
Чтобы получить ключи, мы используем ключевое слово keyof. Получаем объединение всех имен ключей.
Если мы хотим получить значения, мы можем сделать это с помощью синтаксиса квадратных скобок.
Это похоже на синтаксис JS. Только он возвращает типы.
Если мы передадим все подмножество ключей, то получим объединение всех значений этого объекта.
Если мы хотим получить часть, то можем указать это — не все ключи, а какое-то подмножество.
Как и ожидалось, мы получим только те поля, которые соответствуют указанным ключам.
Если полностью свести все к одному случаю, то это одно поле, и один ключ дает одно значение.
Таким образом вы можете получить соответствующее поле.
Давайте посмотрим, как использовать ключи объекта.
Важно понимать, что после ключевого слова расширяется может быть любой допустимый тип.
В том числе сформированные из других дженериков или с использованием ключевых слов.
Давайте посмотрим, как это работает с keyof. Мы определили тип CustomPick. По сути, это практически полная копия библиотечного типа Pick из TypeScript. Что он делает? Он имеет два параметра.
Второе — это не просто какой-то параметр.
Должно быть, это ключи от первого.
Мы видим, что у нас есть расширение keyof из .
Это означает, что это должно быть какое-то подмножество ключей.
Далее для каждого ключа К из этого подмножества пробегаемся по объекту, ставим одно и то же значение и специально убираем необязательность с синтаксисом, за вычетом вопросительного знака.
То есть все поля будут обязательными.
Давайте посмотрим на приложение.
У нас есть объект с именами полей.
Мы можем взять только их подмножество — a, b или c, или все сразу.
Мы взяли a или c. Отображаются только соответствующие значения, но мы видим, что поле a стало обязательным, потому что мы, так сказать, убрали вопросительный знак.
Мы определили этот тип и использовали его.
Никто не мешает нам взять этот дженерик и вставить его в другой дженерик.
Как это произошло? Мы определили еще один тип Custom. Второй параметр расширяет не keyof, а результат использования общего, который мы предоставили справа.
Как он работает, что мы собственно в него переносим? Мы передаем в этот дженерик любой объект и все его ключи.
Это означает, что на выходе будет копия объекта со всеми необходимыми полями.
Эту цепочку вложения дженерика в другой дженерик и так далее можно продолжать до бесконечности, в зависимости от задач, а код можно структурировать.
Генерируйте повторно используемые конструкции в дженерики и так далее.
Указанные аргументы не обязательно должны быть по порядку.
Похоже, что параметр P расширяет клавиши T в универсальном шаблоне CustomPick. Но нам никто не мешал указать его первым параметром, а T — вторым.
TypeScript не всегда последовательно следует за параметрами.
Он смотрит на все параметры, которые мы указали.
Затем он решает некую систему уравнений, и если находит решение типов, удовлетворяющих этой системе, то проверка типов пройдена.
В связи с этим можно получить забавный дженерик, в котором параметры расширяют ключи друг друга: a — ключи b, b — ключи a. Казалось бы, как же так, ключи ключей? Но мы знаем, что строки TypeScript на самом деле являются строками JavaScript, а строки JavaScript имеют свои собственные методы.
Соответственно, подойдет любое строковое имя метода.
Потому что имя строкового метода также является строкой.
Отсюда она и получила свое имя.
Соответственно, мы можем получить такое ограничение, и система уравнений разрешится, если указать нужные типы.
Давайте посмотрим, как это можно использовать в реальности.
Мы используем его для API. Есть сайт, где развертываются приложения Яндекса.
Мы хотим отобразить проект и соответствующий ему сервис.
В примере я взял проект по запуску виртуальных машин qyp для разработчиков.
Мы знаем, что структура этого объекта у нас есть в бэкенде, поэтому берем ее из базы данных.
Но кроме проекта есть и другие объекты: черновики, ресурсы.
И у каждого свои структуры.
Причем мы хотим запросить не весь объект целиком, а пару полей — имя и имя сервиса.
Есть такая возможность; бэкенд позволяет передавать пути и получать неполную структуру.
DeepPartial описан здесь.
Чуть позже мы научимся его проектировать.
Но это значит, что передается не весь объект, а какая-то его часть.
Мы хотим написать какую-нибудь функцию, которая бы запрашивала эти объекты.
Давайте напишем это на JS. Но если присмотреться, то можно увидеть опечатки.
Также в сервисе есть опечатка по типу «Проект», в путях.
Нехорошо, ошибка возникнет во время выполнения.
Вариант TS, похоже, не сильно отличается, за исключением путей.
Но мы покажем, что на самом деле поле Type не может иметь никаких других значений, кроме тех, что у нас на бэкенде.
Поле путей имеет специальный синтаксис, который просто не позволит нам выбрать другие недостающие поля.
Мы используем функцию, в которой просто перечисляем нужные нам уровни вложенности и получаем объект. Фактически, получение путей из этой функции является задачей нашей реализации.
Никакого секрета здесь нет, она использует прокси.
Для нас это не так важно.
Давайте посмотрим, как получить функцию.
У нас есть функция, ее использование.
Есть такая структура.
Сначала мы хотим получить все имена.
Пишем этот тип, где имя соответствует структуре.
Допустим, для проекта мы где-то описываем его тип.
В нашем проекте мы генерируем типизации из файлов protobuf, доступных в общем репозитории.
Далее мы видим, что у нас есть все используемые типы: Project, Draft, Resource. Давайте посмотрим на реализацию.
Давайте по порядку.
Есть функция.
Во-первых, давайте посмотрим, как он параметризуется.
Просто эти ранее описанные имена.
Посмотрим, что он вернет. Он возвращает значения.
Почему это так? Мы использовали синтаксис квадратных скобок.
Но поскольку мы передаем типу одну строку, объединение строковых литералов при их использовании всегда представляет собой одну строку.
Невозможно создать строку, которая одновременно является проектом и ресурсом.
Она всегда одна, и смысл тоже тот же.
Давайте обернем все в DeepPartial. Необязательный тип, необязательная структура.
Самое интересное — это параметры.
Мы устанавливаем их, используя другой дженерик.
Тип, которым параметризуется универсальный параметр, также совпадает с ограничением на функцию.
Он может принимать только тип имени — Проект, Ресурс, Черновик.
ID — это, конечно, строка; нас это не интересует. Вот тот тип, который мы указали, один из трёх.
Интересно, как устроена функция для путей.
Это еще один общий вариант — почему бы нам не использовать его повторно.
По сути, все, что он делает, это просто создает функцию, которая возвращает любой массив, потому что наш объект может иметь поля любого типа, мы не знаем какого.
В этой реализации мы получаем контроль над типами.
Если кому-то это показалось простым, перейдем к структурам управления.
Структуры управления
Мы рассмотрим всего две конструкции, но их будет достаточно, чтобы охватить практически все необходимые нам задачи.Что такое условные типы? Они очень похожи на тройные числа в JavaScript, только для типов.
У нас есть условие, что тип a является подтипом b. Если да, то верните c. Если это не так, верните d. То есть это обычный if, только для типов.
Давайте посмотрим, как это работает. Мы определим тип CustomExclude, который по сути копирует библиотеку Exclude. Он просто выбрасывает нужные нам элементы из объединения типов.
Если a является подтипом b, верните пустое значение, в противном случае верните a. Это странно, если посмотреть, почему это работает с соединениями.
Нам понадобится специальный закон, который гласит: если есть объединение и мы проверяем условия с помощью расширений, то мы проверяем каждый элемент отдельно, а затем объединяем их снова.
Это переходный закон, только для условных типов.
Когда мы используем CustomExclude, мы по очереди просматриваем каждый элемент наблюдения.
a расширяет a, a является подтипом, но возвращает void; b является подтипом a? Нет – возврат б.
c также не является подтипом a, верните c. Затем объединяем то, что осталось, все плюсики, и получаем b и c. Мы выбросили и добились того, чего хотели.
Тот же метод можно использовать для получения всех ключей кортежа.
Мы знаем, что кортеж — это то же самое, что и массив.
То есть в нем есть JS-методы, но они нам не нужны, нужны только индексы.
Соответственно, мы просто удалим названия всех методов из всех ключей кортежа и получим только индексы.
Как нам определить ранее упомянутый тип DeepPartial? Рекурсия используется впервые.
Перебираем все ключи объекта и смотрим.
Является ли значение объектом? Если да, примените его рекурсивно.
Если нет, и это строка или число, оставляем это и делаем все поля необязательными.
Это по-прежнему частичный тип.
Этот рекурсивный вызов и условные типы фактически завершают TypeScript Turing. Но не спешите этому радоваться.
Он даст вам пощечину, если вы попытаетесь реализовать что-то подобное, абстракцию с большой рекурсивностью.
TypeScript отслеживает это и выдает ошибку на уровне своего компилятора.
Вы даже не будете ждать, пока там что-то посчитают. И для таких простых случаев, когда вызов у нас всего один, рекурсия вполне подойдет.
Давайте посмотрим, как это работает. Мы хотим решить проблему исправления поля объекта.
Мы используем виртуальное облако для планирования развертывания приложений, и нам нужны ресурсы.
Допустим, мы взяли ресурсы процессора и ядра.
Всем нужны ядра.
Я упростил пример, и там есть только ресурсы, только ядра, и они представляют собой числа.
Мы хотим создать функцию, которая исправляет их, исправляет значения.
Добавьте ядра или вычтите.
В JavaScript, как вы уже могли догадаться, случаются опечатки.
Здесь мы добавляем число в строку — не очень хорошо.
В TypeScript почти ничего не изменилось, но на самом деле этот элемент управления на уровне IDE будет сообщать вам, что вы не можете передавать ничего, кроме этой строки или определенного числа.
Давайте посмотрим, как этого добиться.
Нам нужно получить такую функцию, и мы знаем, что у нас есть объект такого типа.
Нужно понимать, что мы патчим только номер и поля.
То есть вам нужно получить название только тех полей, которые содержат цифры.
У нас есть только одно поле, и это число.
Давайте посмотрим, как это реализовано в TypeScript.
Мы определили функцию.
У него всего три аргумента: сам объект, который мы исправляем, и имя поля.
Но это не просто имя поля.
Это может быть только имя числового поля.
Сейчас мы узнаем, как это делается.
И сам патчер, который представляет собой чистую функцию.
Есть какая-то обезличенная функция, патч.
Нас интересует не его реализация, а то, как получить такой интересный тип, чтобы по условию получать ключи не только числовых, но и любых полей.
Здесь у нас есть цифры.
Давайте еще раз посмотрим, как это происходит. Перебираем все ключи переданного объекта, затем проделываем следующую процедуру.
Мы видим, что поле объекта является подтипом искомого, то есть числового поля.
Если да, то важно писать не значение поля, а имя поля, иначе вообще пустота, никогда.
Но потом это оказался такой странный объект. Все числовые поля стали иметь имена в качестве значений, а все нечисловые поля стали иметь пустоту.
Далее берем все значения этого странного объекта.
Но поскольку все значения содержат пустоту, а пустота при объединении схлопывается, остаются только те поля, которые соответствуют числовым.
То есть мы получили только необходимые поля.
На примере видно: есть простой объект, поле одно.
Это номер? Да.
Поле — это число, это число? Да.
Последняя строка не является числом.
Мы получаем только необходимые числовые поля.
С этим разобрались.
Самое сложное я оставил напоследок.
Это вывод типа Infer. Захват типа в условной конструкции.
Он неотделим от предыдущей темы, поскольку работает только с условными предложениями.
Как это выглядит? Допустим, мы хотим узнать элементы массива.
Прибыл определенный тип массива, хотелось бы узнать конкретный элемент. Смотрим: мы получили какой-то массив.
Это подтип массива из переменной x. Если да, верните этот x, элемент массива.
Если нет, верните пустоту.
В этом случае вторая ветвь никогда не будет выполнена, поскольку мы параметризовали тип каким-либо массивом.
Конечно, это будет массив чего-то, потому что любой массив не может иметь элементов.
Если мы передадим массив строк, строка будет возвращена, как и ожидалось.
И важно понимать, что мы определяем не просто тип.
По массиву строк визуально понятно: строки там есть.
А вот с кортежем все не так просто.
Нам важно знать, что определен минимально возможный супертип.
Понятно, что все массивы являются как бы подтипами массива с любым или с неизвестным.
Эти знания нам ничего не дают. Нам важно знать минимум возможного.
Допустим, мы передаем кортеж.
На самом деле кортежи тоже являются массивами, но как узнать, что представляют собой элементы этого массива? Если существует кортеж числовой строки, то на самом деле это массив.
Но элемент должен иметь один тип.
А если есть и строка, и число, то будет объединение.
TypeScript выведет это, и в этом примере мы получим именно объединение строки и числа.
Вы можете использовать не только захват в одном месте, но и столько переменных, сколько захотите.
Допустим, мы определяем тип, который просто меняет местами элементы кортежа: первый на второй.
Берем первый элемент, второй и меняем их местами.
Но слишком сильно с этим играть не рекомендуется.
Обычно для 90% задач достаточно всего одного типа хвата.
Давайте посмотрим пример.
Задача: нам нужно показать в зависимости от состояния запроса либо хороший вариант, либо плохой.
Вот скриншоты из нашей службы развертывания приложений.
Какая-то сущность, ReplicaSet. Если запрос от бэкэнда возвращает ошибку, вам необходимо его отрендерить.
Существует также API для бэкэнда.
Давайте посмотрим, какое отношение к этому имеет Infer. Мы знаем, что используем, во-первых, redux, а во-вторых, redux thunk. И нам нужно преобразовать библиотеку, чтобы получить эту функцию.
У нас есть плохой путь и хороший путь.
И мы знаем, что хороший путь к extraReducers в наборе инструментов redux выглядит так.
Мы знаем, что есть PayLoad, и хотим вытащить из бэкенда кастомные типы, которые нам прислали, но не только это, но и информацию о хорошем или плохом запросе: есть ли там ошибка или нет. Нам нужен общий результат для этого вывода.
Я не сравниваю JavaScript, потому что это не имеет смысла.
В JavaScript в принципе нельзя никак управлять типами и полагаться только на память.
Здесь нет плохого варианта, потому что его просто нет. Мы знаем, что нам нужен этот тип.
Но дело не только в том, что у нас есть действия.
Нам нужно вызвать диспетчерскую службу с помощью этого действия.
И нам нужно вот это представление, где нужно отобразить ошибку на основе ключа запроса.
То есть вам нужно добавить такой дополнительный функционал поверх redux thunk с помощью метода withRequestKey. Конечно, у нас есть этот метод, но у нас также есть оригинальный метод API — getReplicaSet. Это где-то записано, и нам нужно переопределить redux thunk с помощью какого-то адаптера.
Давайте посмотрим, как это сделать.
Нам нужно получить такую функцию.
Это переходник с дополнительной функциональностью.
Звучит страшно, но не пугайтесь, сейчас мы разберем это по частям, чтобы было понятно.
Существует адаптер, расширяющий исходный тип библиотеки.
Мы просто добавляем дополнительный метод withRequestKey и собственный вызов этого типа библиотеки.
Давайте разберемся, в чем главная особенность дженерика и какие параметры используются.
Первый — это просто наш API, объект с методами.
Мы можем использовать getReplicaSet, получать проекты, ресурсы и т. д. В текущем методе мы используем конкретный метод, а второй параметр — это просто имя метода.
Далее мы используем параметры запрашиваемой нами функции, используем тип библиотеки «Параметры», это тип TypeScript. Аналогично, для ответа от бэкенда мы используем библиотечный тип ReturnType. Это то, для чего вернулась функция.
Далее мы просто передаем наш пользовательский вывод в тип AsyncThunk, предоставленный нам библиотекой.
Но что это за вывод? Это еще один дженерик.
На самом деле это выглядит просто.
Сохраняем не только ответ от сервера, но и наши параметры, которые мы передали.
Просто чтобы отслеживать их в Редукторе.
Далее мы рассмотрим withRequestKey. Наш метод просто добавляет ключ.
Что он возвращает? Тот же адаптер, потому что мы можем использовать его повторно.
Нам вообще не обязательно писать с помощью RequestKey. Это просто дополнительный функционал.
Он оборачивает и рекурсивно возвращает нам тот же адаптер, и мы кидаем туда то же самое.
Наконец, давайте посмотрим, как вывести в Редюсер то, что нам вернул этот переходник.
У нас есть этот адаптер.
Главное помнить, что есть четыре параметра: API, метод API, параметры (входные) и выходные.
Нам нужно найти выход. Но мы помним, что наш вывод индивидуален: и ответ сервера, и параметр запроса.
Как это сделать с помощью Infer? Видим, что на вход подается этот адаптер, но он вообще любой: любой, любой, любой, любой.
Мы должны вернуть этот тип, он выглядит так, ответ сервера и параметры запроса.
И смотрим, где должен быть вход. На третьем.
Здесь мы размещаем нашу текстовую ручку.
Мы получаем вход. Аналогично, на четвертом месте – выход. TypeScript основан на структурной типизации.
Он разбирает эту конструкцию и понимает, что вход здесь, на третьем месте, а выход на четвёртом.
И возвращаем нужные типы.
Вот как мы добились вывода типа; у нас есть доступ к ним уже в самом Редюсере.
В JavaScript сделать это в принципе невозможно.
Теги: #JavaScript #Промышленное программирование #typescript #печатать #redux #generics #строгая типизация #redux-thunk
-
Как Создать Список, Используя Контент Plr
19 Oct, 24 -
Зарядка Аккумулятора Менее Чем За 30 Секунд
19 Oct, 24 -
Что Программируют Программисты?
19 Oct, 24