Привет, друзья! Представляю вашему вниманию перевод еще нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript. Тип можно понимать как коллекцию или набор значений. Например, тип number можно считать множеством (set) всех чисел. 1.0, 68 принадлежат этому множеству, а "bytefer" нет, поскольку типом "bytefer" является string. Тоже самое справедливо и в отношении объектных типов, которые можно понимать как коллекции объектов. Например, тип Point в приведенном ниже примере представляет множество объектов со свойствами x и y, значениями которых являются числа, а тип Named представляет множество объектов со свойством name, значением которого является строка: 
 Согласно теории множеств, (set theory) множество, содержащее элементы, принадлежащие как множеству A, так и множеству B, называется пересечением (intersection) множеств A и B: 
 При пересечении Point и Named создается новый тип. Объект нового типа принадлежит как Point, так и Named. TS предоставляет оператор & для реализации операции пересечения нескольких типов. Результирующий новый тип называется пересечением (intersection type). Правила применения оператора &: 
 Типы any и never являются особенными. Не считая типа never, пересечением любого типа с any является any. Рассмотрим пересечение типов Point и Named: 
 Новый тип NamedPoint содержит свойства x, y и name. Но что произойдет при пересечении объектов, содержащих одинаковые свойства со значениями разных типов? В приведенном примере интерфейсы X и Y содержат свойство c, но значения этого свойства имеют разные типы. Может ли в данном случае значение атрибута c в типах XY и YX быть строкой или числом? 
 Почему типом значения свойства c является never? Дело в том, что значение c должно быть одновременно и строкой, и числом (string & number). Но такого типа не существует, поэтому типом значения c становится never. Что произойдет в аналогичном случае с непримитивными значениями? Рассмотрим пример: 
 При пересечении нескольких типов в случае, когда существует одинаковое свойство и его значением является объект, значения объединяются в соответствии с указанными выше правилами. Кроме объектных типов, пересечение может применяться в отношении функциональных типов: 
 При вызове f(1, "bytefer") возникает ошибка: В данном случае компилятор TS обращается к перегрузкам функции (function overloading) для выполнения операции пересечения. Для решения проблемы можно определить новый тип функции F3 со следующей сигнатурой: 
 С помощью пересечения можно реализовать некоторые полезные утилиты типа (utility types). Например, реализуем утилиту PartialByKeys, которая делает типы значений указанных ключей объекта опциональными: 
 Аналогичным способом можно реализовать утилиту RequiredByKeys. Знаете ли вы, как извлечь тип элементов из массива типа T0 или тип, возвращаемый функцией типа T1? Для этого можно использовать технику поиска совпадений (pattern matching), предоставляемую TS — сочетание условных типов (conditional types) и ключевого слова infer. Условные типы позволяют определять отношения между типами, с их помощью можно определять совпадение типов. infer используется для определения переменной типа для хранения типа, захваченного (captured) в процессе поиска совпадений. Рассмотрим, как можно захватить тип элементов массива типа T0: 
 В приведенном примере T extends (infer U)[] ? U : T — это синтаксис условных типов, а infer U — это инструкция расширения (extends clause), представляющая новую переменную типа U для хранения предполагаемого или выводимого (inferred) типа. Для лучшего понимания рассмотрим поток выполнения (execution flow) утилиты UnpackedArray: 
 Обратите внимание: infer может использоваться только в инструкции расширения условного типа. Переменная типа, объявленная посредством infer, доступна только в истинной ветке (true branch) условного типа. 
 Рассмотрим, как получить тип, возвращаемый функцией T1: Легко, не правда ли? Обратите внимание: когда речь идет о перезагрузках функции, TS использует последнюю сигнатуру вызова (call signature) для вывода типа. 
 В предыдущей статье мы рассмотрели условные цепочки, которые позволяют реализовать более мощную утилиту типа: 
 В приведенном примере утилита Unpacked позволяет легко извлекать типы элементов массивов, а также типы, возвращаемые функциями и промисами, благодаря условным типам и условным цепочкам. Аналогичным способом можно вывести тип ключа объекта. Рассмотрим пример: 
 В приведенном примере используется две переменных типа: U и R, представляющие типы свойств объекта id и name, соответственно. При совпадении типов возвращается кортеж (tuple). Что будет, если определить только переменную типа U? Давайте проверим: 
 Как видите, тип U4 возвращает объединение (union) типов строки и числа. Почему так происходит? Дело в том, что при наличии нескольких кандидатов для одной и той же переменной типа в ковариантной позиции (covariant position), предполагается, что результирующий тип является объединением. Тем не менее, в аналогичной ситуации, но в контрвариативной позиции (contravariant position), предполагается, что результирующий тип является пересечением (intersection): 
 В приведенном примере тип U5 возвращает пересечение типов строки и числа, поэтому результирующим типом будет never. Наконец, позвольте представить вам новую возможность, появившуюся в TS 4.7, которая делает процесс вывода типов более согласованным. Но сначала рассмотрим пример: Утилита типа FirstIsString использует возможности условных типов, условных цепочек и infer. В первом условии проверяется, что переданный тип T является непустым кортежем. Там же определяется переменная типа S для хранения типа первого элемента захваченного в процессе поиска совпадений кортежа. Во втором условии проверяется, является ли переменная S подтипом (subtype) строки. Если является, возвращается string, иначе возвращается never. 
 Как видите, утилита FirstIsString прекрасно справляется со своей задачей. Но можем ли мы ограничиться одним условным типом для достижения того же результата? TS 4.7 позволяет добавлять опциональную инструкцию расширения в предполагаемый тип для определения явных ограничений (explicit constraints) переменной типа: Напоследок реализуем утилиту для преобразования объединения в пересечение: 
 При разработке веб-страниц мы часто используем компоненты Tooltip или Popover для отображения каких-либо сообщений. Для удовлетворения различных сценариев эти компоненты должны позволять устанавливать позицию сообщения, например, top, bottom, left, right и т.д. 
 Соответствующий тип строковых литералов (string literals) можно определить следующим образом: Для многих сценариев этого будет достаточно. Но что если мы хотим расширить список доступных позиций? Например, что если сообщение должно отображаться в верхнем правом углу? 
 Определим тип Placement посредством расширения типа Side: Глядя на эти строковые литералы, нетрудно заметить дублирующийся код, такой как -start и -end. Кроме того, при определении большого количества литералов легко допустить очепятку. Существует ли лучший способ решения данной задачи? В TS 4.1 были представлены шаблонные литеральные типы (template literal types), позволяющие делать так: Как и шаблонные строки (template strings/literals) в JS, шаблонные типы заключаются в обратные кавычки (``) и могут содержать заменители (placeholders) в форме ${T}. Передаваемый тип может быть string, number, boolean или bigint. 
 Шаблонные типы позволяют объединять (concatenate) строковые литералы и конвертировать литералы непримитивных типов в соответствующие строковые литералы. Вот парочка примеров: 
 Вопрос: каким будет результат, если тип, переданный в утилиту EventName или Concat будет объединением? Давайте проверим: 
 Почему генерируется такой тип? Это объясняется тем, что в случае шаблонных типов объединения в заменителях распределяются по шаблону: 
 А в случае с несколькими заменителями, как в утилите Concat, объединения разрешаются в векторное произведение (cross product): 
 Работая с шаблонными типами, мы также можем применять встроенные утилиты типов для работы со строками, такие как Uppercase, Lowercase, Capitalize и Uncapitalize: Возможности шаблонных типов являются очень мощными. В сочетании с условными типами и ключевым словом infer можно реализовать, например, такую утилиту вывода типа (type inference): TS 4.1 также позволяет использовать оговорку as для переименования ключей при сопоставлении типов: 
 Тип NewKeyType должен быть подтипом объединения string | number | symbol. В процессе переименования ключей посредством шаблонных типов можно реализовать некоторые полезные утилиты. Например, определим утилиту Getters для генерации типов геттеров для соответствующего объекта: 
 В приведенном примере поскольку тип, возвращаемый keyof T, может содержать тип symbol, а утилита Capitilize требует, чтобы обрабатываемый тип был подтипом строки, необходимо выполнить фильтрацию типов с помощью оператора &. Попробует реализовать более сложную утилиту. Например, для извлечения типов из объекта с произвольными вложенными свойствами: 
 Открываем файл определений *.d.ts и видим там ключевое слово declare. Знаете ли вы, для чего оно используется? В TS-проектах мы часто импортируем сторонние JS-SDK с помощью тега script, например, так импортируется Google Maps: Обращаемся к этому API после инициализации: 
 Несмотря на то, что все делается в соответствии с официальной документацией, TS выводит сообщение об ошибке. Это связано с тем, что компилятор TS не может распознать глобальную переменную google. Как решить эту задачу? Использовать ключевое слово declare для определения глобальной переменной google: 
 Но почему мы без проблем можем использовать такие глобальные переменные, как JSON, Math или Object? Дело в том, что эти переменные уже объявлены с помощью declare в файле определений lib.es5.d.ts: declare также может использоваться для определения глобальных функций, классов или перечислений (enums). Такие функции, как eval, isNaN, encodeURI и parseInt также определяются в lib.es5.d.ts: Следует отметить, что при определении глобальной функции мы не включаем в определение конкретную реализацию этой функции. На самом деле в большинстве случаев у нас необходимости определять глобальные переменные, предоставляемые сторонними библиотеками, самостоятельно. Для поиска соответствующих типов можно обратиться к поисковику TypeScript или к проекту DefinitelyTypes. 
 Устанавливаем типы для Google Maps в качестве зависимости для разработки: Для npm-пакета foo пакет с типами чаще всего будет называться @types/foo. Например, для библиотеки jquery пакет с типами называется @types/jquery. Посмотрим на использование declare в реальном проекте. Создаем шаблон Vue-TS-проекта с помощью Vite: Открываем файл client.d.ts: 
 Видим определения модулей css, jpg и ttf. Зачем нам эти модули? Без их определения компилятор TS не сможет их распознать и будет выводить сообщения об ошибках: 
 С помощью символа * можно определить один модуль для конкретного типа файлов вместо того, чтобы указывать каждый ресурс по отдельности: 
 TS также позволяет расширять типы, определенные в существующем модуле, с помощью declare. Например, определим свойство $axios в каждом экземпляре Vue-компонента: Добавляем свойство $axios в каждый экземпляр компонента с помощью свойства globalProperties объекта с настройками: И используем его в компоненте: Надеюсь, что вы, как и я, нашли для себя что-то интересное. Благодарю за внимание и happy coding!  JavaScript Developer