Манипуляции с типами
- Работа с переменными типа в дженериках
- Общие типы
- Общие классы
- Ограничения дженериков
- Использование типов параметров в ограничениях дженериков
- Использование типов класса в дженериках
- Ограничения
- Ограничения условных типов
- Предположения в условных типах
- Распределенные условные типы (distributive conditional types)
- Модификаторы связывания (mapping modifiers)
-
Повторное связывание ключей с помощью
as
- Строковые объединения в типах
- Предположения типов с помощью шаблонных литералов
- Внутренние типы манипуляций со строками (intrisic string manipulation types)
Система типов TS
позволяет создавать типы на основе других типов.
Простейшей формой таких типов являются дженерики или общие типы (generics). В нашем распоряжении также имеется целый набор операторов типа. Более того, мы можем выражать типы в терминах имеющихся у нас значений.
#
ДженерикиСоздадим функцию identity
, которая будет возвращать переданное ей значение:
Для того, чтобы сделать эту функцию более универсальной, можно использовать тип any
:
Однако, при таком подходе мы не будем знать тип возвращаемого функцией значения.
Нам нужен какой-то способ перехватывать тип аргумента для обозначения с его помощью типа возвращаемого значения. Для этого мы можем воспользоваться переменной типа, специальным видом переменных, которые работают с типами, а не со значениями:
Мы используем переменную Type
как для типа передаваемого функции аргумента, так и для типа возвращаемого функцией значения.
Такие функции называют общими (дженериками), поскольку они могут работать с любыми типами.
Мы можем вызывать такие функции двумя способами. Первый способ заключается в передаче всех аргументов, включая аргумент типа:
В данном случае принимаемым и возвращаемым типами является строка.
Второй способ заключается в делегировании типизации компилятору:
Второй способ является более распространенным. Однако, в более сложных случаях может потребоваться явное указание типа, как в первом примере.
#
Работа с переменными типа в дженерикахЧто если мы захотим выводить в консоль длину аргумента arg
перед его возвращением?
Мы получаем ошибку, поскольку переменные типа указывают на любой (а, значит, все) тип, следовательно, аргумент arg
может не иметь свойства length
, например, если мы передадим в функцию число.
Изменим сигнатуру функции таким образом, чтобы она работала с массивом Type
:
Теперь наша функция стала дженериком, принимающим параметр Type
и аргумент arg
, который является массивом Type
, и возвращает массив Type
. Если мы передадим в функцию массив чисел, то получим массив чисел.
Мы можем сделать тоже самое с помощью такого синтаксиса:
#
Общие типыТип общей функции (функции-дженерика) похож на тип обычной функции, в начале которого указывается тип параметра:
Мы можем использовать другое название для параметра общего типа:
Мы также можем создавать общие типы в виде сигнатуры вызова типа объектного литерала:
Это приводит нас к общему интерфейсу:
Для того, чтобы сделать общий параметр видимым для всех членов интерфейса, его необходимо указать после названия интерфейса:
Кроме общих интерфейсов, мы можем создавать общие классы.
Обратите внимание
Общие перечисления (enums) и пространства имен (namespaces) создавать нельзя.
#
Общие классыОбщий класс имеет такую же форму, что и общий интерфейс:
В случае с данным классом мы не ограничены числами. Мы вполне можем использовать строки или сложные объекты:
Класс имеет две стороны с точки зрения типов: статическую сторону и сторону экземпляров. Общие классы являются общими только для экземпляров. Это означает, что статические члены класса не могут использовать тип параметра класса.
#
Ограничения дженериковИногда возникает необходимость в создании дженерика, работающего с набором типов, когда мы имеем некоторую информацию о возможностях, которыми будет обладать этот набор. В нашем примере loggingIdentity
мы хотим получать доступ к свойству length
аргумента arg
, но компилятор знает, что не каждый тип имеет такое свойство, поэтому не позволяет нам делать так:
Мы хотим, чтобы функция работала с любым типом, у которого имеется свойство length
. Для этого мы должны создать ограничение типа.
Нам необходимо создать интерфейс, описывающий ограничение. В следующем примере мы создаем интерфейс с единственным свойством length
и используем его с помощью ключевого слова extends
для применения органичения:
Поскольку дженерик был ограничен, он больше не может работать с любым типом:
Мы должны передавать ему значения, отвечающие всем установленным требованиям:
#
Использование типов параметров в ограничениях дженериковМы можем определять типы параметров, ограниченные другими типами параметров. В следующем примере мы хотим получать свойства объекта по их названиям. При этом, мы хотим быть уверенными в том, что не извлекаем несуществующих свойств. Поэтому мы помещаем ограничение между двумя типами:
#
Использование типов класса в дженерикахПри создании фабричных функций с помощью дженериков, необходимо ссылаться на типы классов через их функции-конструкторы. Например:
В более сложных случаях может потребоваться использование свойства prototype
для вывода и ограничения отношений между функцией-конструктором и стороной экземляров типа класса:
Данный подход часто используется в миксинах или примесях.
keyof
#
Оператор типа Оператор keyof
"берет" объектный тип и возвращает строковое или числовое литеральное объединение его ключей:
Если типом сигнатуры индекса (index signature) типа является string
или number
, keyof
возвращает эти типы:
Обратите внимание
Типом M
является string | number
. Это объясняется тем, что ключи объекта в JS
всегда преобразуются в строку, поэтому obj[0]
- это всегда тоже самое, что obj['0']
.
Типы keyof
являются особенно полезными в сочетании со связанными типами (mapped types), которые мы рассмотрим позже.
typeof
#
Оператор типа JS
предоставляет оператор typeof
, который можно использовать в контексте выражения:
В TS
оператор typeof
используется в контексте типа для ссылки на тип переменной или свойства:
В сочетании с другими операторами типа мы можем использовать typeof
для реализации нескольких паттернов. Например, давайте начнем с рассмотрения предопределенного типа ReturnType<T>
. Он принимает тип функции и производит тип возвращаемого функцией значения:
Если мы попытаемся использовать название функции в качестве типа параметра ReturnType
, то получим ошибку:
Запомните: значения и типы - это не одно и тоже. Для ссылки на тип значения f
следует использовать typeof
:
#
ОграниченияTS
ограничивает виды выражений, на которых можно использовать typeof
.
typeof
можно использовать только в отношении идентификаторов (названий переменных) или их свойств. Это помогает избежать написания кода, который не выполняется:
#
Типы доступа по индексу (indexed access types)Мы можем использовать тип доступа по индексу для определения другого типа:
Индексированный тип - это обычный тип, так что мы можем использовать объединения, keyof
и другие типы:
При попытке доступа к несуществующему свойству возникает ошибка:
Другой способ индексации заключается в использовании number
для получения типов элементов массива. Мы также можем использовать typeof
для перехвата типа элемента:
Обратите внимание
Мы не можем использовать const
, чтобы сослаться на переменную:
Однако, в данном случае мы можем использовать синоним типа (type alias):
#
Условные типы (conditional types)Обычно, в программе нам приходится принимать решения на основе некоторых входных данных. В TS
решения также зависят от типов передаваемых аргументов. Условные типы помогают описывать отношения между типами входящих и выходящих данных.
Условные типы имеют форму, схожую с условными выражениями в JS
(условие ? истинноеВыражение : ложноеВыражение
).
Когда тип слева от extends
может быть присвоен типу справа от extends
, мы получаем тип из первой ветки (истинной), в противном случае, мы получаем тип из второй ветки (ложной).
В приведенном примере польза условных типов не слишком очевидна. Она становится более явной при совместном использовании условных типов и дженериков (общих типов).
Рассмотрим такую функцию:
Перегрузки createLabel
описывают одну и ту же функцию, которая делает выбор на основе типов входных данных.
Обратите внимание
- Если библиотека будет выполнять такую проверку снова и снова, это будет не очень рациональным.
- Нам пришлось создать 3 перегрузки: по одной для каждого случая, когда мы уверены в типе (одну для
string
и одну дляnumber
), и еще одну для общего случая (string
илиnumber
). Количество перегрузок будет увеличиваться пропорционально добавлению новых типов.
Вместо этого, мы можем реализовать такую же логику с помощью условных типов:
Затем мы можем использовать данный тип для избавления от перегрузок:
#
Ограничения условных типовЧасто проверка в условном типе дает нам некоторую новую информацию. Подобно тому, как сужение с помощью защитников или предохранителей типа (type guards) возвращает более конкретный тип, инстинная ветка условного типа ограничивает дженерики по типу, который мы проверяем.
Рассмотрим такой пример:
В данном случае возникает ошибка, поскольку TS
не знает о существовании у T
свойства message
. Мы можем ограничить T
, и тогда TS
перестанет "жаловаться":
Но что если мы хотим, чтобы MessageOf
принимал любой тип, а его "дефолтным" значением был тип never
? Мы можем "вынести" ограничение и использовать условный тип:
Находясь внутри истинной ветки, TS
будет знать, что T
имеет свойство message
.
В качестве другого примера мы можем создать тип Flatten
, который распаковывает типы массива на типы составляющих его элементов, но при этом сохраняет их в изоляции:
Когда Flatten
получает тип массива, он использует доступ по индексу с помощью number
для получения типа элемента string[]
. В противном случае, он просто возвращает переданный ему тип.
#
Предположения в условных типахМы использовали условные типы для применения ограничений и извлечения типов. Это является настолько распространенной операцией, что существует особая разновидность условных типов.
Условные типы предоставляют возможность делать предположения на основе сравниваемых в истинной ветке типов с помощью ключевого слова infer
. Например, мы можем сделать вывод относительно типа элемента во Flatten
вместо его получения вручную через доступ по индексу:
В данном случае мы использовали ключевое слово infer
для декларативного создания нового дженерика Item
вместо извлечения типа элемента T
в истинной ветке. Это избавляет нас от необходимости "копаться" и изучать структуру типов, которые нам необходимы.
Мы можем создать несколько вспомогательных синонимов типа (type aliases) с помощью infer
. Например, в простых случаях мы можем извлекать возвращаемый тип из функции:
При предположении на основе типа с помощью нескольких сигнатур вызова (такого как тип перегруженной функции), предположение выполняется на основе последней сигнатуры. Невозможно произвести разрешение перегрузки на основе списка типов аргументов.
#
Распределенные условные типы (distributive conditional types)Когда условные типы применяются к дженерикам, они становятся распределенными при получении объединения (union). Рассмотрим следующий пример:
Если мы изолируем объединение в ToArray
, условный тип будет применяться к каждому члену объединения.
Здесь StrOrNumArray
распределяется на:
и применяется к каждому члену объединения:
что приводит к следующему:
Обычно, такое поведение является ожидаемым. Для его изменения можно обернуть каждую сторону extends
в квадратные скобки:
#
Связанные типы (mapped types)Связанные типы основаны на синтаксисе сигнатуры доступа по индексу, который используется для определения типов свойств, которые не были определены заранее:
Связанный тип - это общий тип, использующий объединение, созданное с помощью оператора keyof
, для перебора ключей одного типа в целях создания другого:
В приведенном примере OptionsFlag
получит все свойства типа Type
и изменит их значения на boolean
.
#
Модификаторы связывания (mapping modifiers)Существует два модификатора, которые могут применяться в процессе связывания типов: readonly
и ?
, отвечающие за иммутабельность (неизменность) и опциональность, соответственно.
Эти модификаторы можно добавлять и удалять с помощью префиксов -
или +
. Если префикс отсутствует, предполагается +
.
as
#
Повторное связывание ключей с помощью В TS
4.1 и выше, можно использовать оговорку as
для повторного связывания ключей в связанном типе:
Для создания новых названий свойств на основе предыдущих можно использовать продвинутые возможности, такие как типы шаблонных литералов (см. ниже):
Ключи можно фильтровать с помощью never
в условном типе:
Связанные типы хорошо работают с другими возможностями по манипуляции типами, например, с условными типами. В следующем примере условный тип возвращает true
или false
в зависимости от того, содержит ли объект свойство pii
с литерально установленным true
:
#
Типы шаблонных литералов (template literal types)Типы шаблонных литералов основаны на типах строковых литералов и имеют возможность превращаться в несколько строк через объединения.
Они имеют такой же синтаксис, что и шаблонные литералы в JS
, но используются на позициях типа. При использовании с конкретным литеральным типом, шаблонный литерал возвращает новый строковый литерал посредством объединения содержимого:
Когда тип используется в интерполированной позиции, он является набором каждого возможного строкого литерала, который может быть представлен каждым членом объединения:
Для каждой интерполированной позиции в шаблонном литерале объединения являются множественными:
Большие строковые объединения лучше создавать отдельно, но указанный способ может быть полезным в простых случаях.
#
Строковые объединения в типахМощь шаблонных строк в полной мере проявляется при определении новой строки на основе существующей внутри типа.
Например, обычной практикой в JS
является расширение объекта на основе его свойства. Создадим определение типа для функции, добавляющей поддержку для функции on
, которая позволяет регистрировать изменения значения:
Обратите внимание
on
регистрирует событие firstNameChanged
, а не просто firstName
.
Шаблонные литералы предоставляют способ обработки такой операции внутри системы типов:
При передаче неправильного свойства возникает ошибка:
#
Предположения типов с помощью шаблонных литераловЗаметьте, что в последних примерах типы оригинальных значений не использовались повторно. В функции обратного вызова использовался тип any
. Типы шаблонных литералов могут предполагаться на основе заменяемых позиций.
Мы можем переписать последний пример с дженериком таким образом, что типы будут предполагаться на основе частей строки eventName
:
Здесь мы реализовали on
в общем методе.
При вызове пользователя со строкой firstNameChanged
, TS
попытается предположить правильный тип для Key
. Для этого TS
будет искать совпадения Key
с "контентом", находящимся перед Changed
, и дойдет до строки firstName
. После этого метод on
сможет получить тип firstName
из оригинального объекта, чем в данном случае является string
. Точно также при вызове с ageChanged
, TS
обнаружит тип для свойства age
, которым является number
.
#
Внутренние типы манипуляций со строками (intrisic string manipulation types)TS
предоставляет несколько типов, которые могут использоваться при работе со строками. Эти типы являются встроенными и находятся в файлах .d.ts
, создаваемых TS
.
Uppercase<StringType>
- переводит каждый символ строки в верхний регистр
Lowercase<StringType>
- переводит каждый символ в строке в нижний регистр
Capitilize<StringType>
- переводит первый символ строки в верхний регистр
Uncapitilize<StringType>
- переводит первый символ строки в нижний регистр
Вот как эти типы реализованы: