Типы, как множества. Объединение, пересечения, литералы
Last updated
Was this helpful?
Last updated
Was this helpful?
Ключевая концепция TypeScript заключается в том, что любой тип можно предствить как множество значений. Например, тип number
- это множество всех чисел. Иными словами, переменной типа number
можно присвоить любое числовое значение, так как каждое число входит во множество всех чисел. Чего нельзя сказать про любую строку. Не существует ни одной строки, которая бы принадлежала множеству number
, но при этом любая строка принадлежит множеству string
.
Как TypeScript понимает, что какое-либо выражение валидно с точки зрения типов? Рассмотрим следующий пример.
Мы явно типизировали переменные a
и b
- указали, что эти переменные являются хранилищами для значений, которые принадлежат множеству number
. При этом, когда мы присваиваем переменной b
значение переменной a
, TypeScript проверяет, является ли множество значений (тип), которые могут находиться в переменной a
, множества значений, которые можно присвоить переменной b
.
Что будет происходить в таком случаи
Теперь тип переменной a
- это литеральный тип "str"
, который принадлежит множеству string
. Множество number
не содержит значение "str"
. Поэтому значение переменной a
нельзя присваивать переменой b
.
В этом заключается ключевая механика TypeScript. При выполнении различных операций: присваивание, сложение, вычитание, вызов функций, создание объектов и т.д. - TypeScript проверяет, является ли множество значений одного операнда подмножеством множества значений другого операнда (совместимы ли типы операндов).
Рассмотрим следующий код:
Мы указали, что переменная a
- это кортеж из 3 элементов и тип каждого элемента - это number
. Переменная b
- массив из любого количества элементов, при этом тип каждого элемента - это number
. Получается, что кортеж [number, number, number]
является подмножеством массива number[]
и операция присваивания может быть выполнена. Почему? Потому что любое значение, которое можно присвоить переменной a
можно присвоить и переменной b
. Получается, что кортеж [number, number, number]
является более строгим условием для массива number[]
.
Однако обратная операция невозможна. Логично, что существует хотя бы одно значение, которое принадлежит множеству number[]
и не принадлежит множеству [number, number, number]
. Поэтому следующий код содержит ошибку:
Ранее мы утверждали следующее.
На самом деле это не так. Литерал 10
имеет тип 10
, а не number
. Это максимально точное множество значений для этого литерала - единичное множество, состоящее из значения 10. Такие типы называются литеральными или типами-литералами. В TypeScript можно указывать переменным тип, состоящий из одного единственного значения:
При этом все эти единичные множества являются подмножествами более крупных множеств. Поэтому следующие выражения не содержат ошибок:
По аналогии с массивами и кортежами - обратная операция содержит ошибку:
Тип, который получится при автовыводе, зависит от того, что вы используете при объявлении переменной: const
, let
или var
. Если вы используете const
для объявления переменной с неизменяемым значением, то TypeScript выводит литеральный тип.
Однако, если использовать let
или var
при объявлении переменной, то TypeScript выведет более "крупный" тип.
Почему так происходит? Такое поведение TypeScript объясняется тем, что он выводит тип, который гарантировано бы соответствовал тому множеству значений, которое потенциально можно положить в эту переменную. В случаи объявления переменных, TypeScript опирается на JavaScript. А JavaScript в свою очередь гарантирует, что переменной, которая объявлена с использованием const
нельзя будет изменить значение. Это означает, что множество значений, которые потенциально могут находиться в этой переменной, состоит из одного единственного значения и это то значение, которое задали во время инициализации переменной. Поэтому TypeScript может вывести литеральный тип.
JavaScript не может гарантировать, что переменная, объявленная через let
или var
, не изменит свое значение далее в программе. Соответственно и TypeScript не может гарантировать, что использование литерального типа опишет все возможные значения этой переменной.
По схожим причинам автовывод типов для значений массива (например, [1, 2, 3]
) выводит массив (для примера, number[]
), а не кортеж (для примера, [number, number, number]
). Даже используя const
в объявлении переменной, которой присваивается массив, JavaScript не может гарантировать, что внутреннее состояние массива не поменяется. Далее в программе вы сможете изменять этот массив как вам угодно: подменять значения элементов, добавлять новые элементы, удалять старые и т.д. Поэтому TypeScript не выводит литеральный тип [1, 2, 3]
и даже не кортеж [number, number, number]
, а именно массив number[]
.
В JavaScript одна и та же переменная может содержать в себе значение разных типов. В TypeScript такие переменные можно описать несколькими способами. Один из таких способов - использовать тип any
.
any
- особенный тип в TypeScript. С одной стороны этот тип является множеством абсолютно всех возможных значений. Это означает, что в переменную с типом any
можно записать абсолютно любое значение.
С другой стороны, тип any
является подмножеством абсолютно любого типа. Не важно, что на самом деле находится в переменной с типом any
, с этой переменной можно совершать любые операции.
Можно задавать any
, как тип элементов массива или кортежа. У переменной такого типа будут доступны методы и поля массива, но в качестве элементов могут выступать значения любых типов.
Чем чревато использование any
? Для значений с типом any
TypeScript выключает проверку типов, то есть работает как JavaScript. Все преимущества типизации TypeScript теряются. Поэтому использовать any
нужно с осторожностью. Лучше стараться описывать тип при помощи синтаксиса TypeScript, а не использовать any
. Однако бывают ситуации, когда без any
не обойтись. Например, десериализация из JSON, взаимодействие с JavaScript кодом или не типизированными импортами webpack.
Как описать тип переменной в которую можно записывать значения разных типов, при этом не используя any
? Для этого можно воспользоваться операцией объединения типов.
Для того, чтобы использовать множества значений только двух типов, мы должны сказать TypeScript объединить два множества. Это делается при помощи оператора |
.
Переменная fontWeight
имеет тип number | string
. Это означает, что в эту переменную можно записывать значение из множества number
ИЛИ из множества string
.
Усложним условие и добавим возможность записывать в эту переменную значение null
и undefined
. Для этого нам нужно объединить не 2 множества, а 4: number
, string
, единичное множество null
и единичное множество undefined
.
Мы использовали операцию объединения типов 3 раза: сначала объединили тип number
и string
, затем результат объединения первых двух типов объединили с литеральным типом null
, а затем с undefined
. Получившийся тип сильно лучше, чем any
, однако у него есть изъяны. Например:
Используя типы string
и number
мы не ограничиваем строки и числа, которые можно присваивать переменной. Но мы знаем, что можно использовать литеральные типы, так как они тоже являются множествами, пусть и единичными.
Как объединение работает с массивами и кортежами? Рассмотрим пример
В данном случаи типы у переменных a
и b
задаются как объединение множества number[]
и string[]
. Это означает, что этим переменным можно присвоить или массив чисел, или массив строк. Этим переменным нельзя присвоить массив, первый элемент которого будет number
, а второй string
.
Однако, такое значение можно присвоить этой переменной, если записать тип по-другому.
В этом случаи мы указали тип переменной a
, как массив, каждый элемент которого принадлежит типу number
или типу string
.
По аналогии с объединением типов, в TypeScript существует операция пересечения типов. Она обозначается символом &
. Если мы объявим переменную const a: number & string
, то ей нужно присвоить такое значение, которое одновременно принадлежит множеству number
и string
, то есть является и числом, и строкой одновременно. Такого значения не существует. С точки зрения множеств мы пытаемся пересечь два множества, которые не пересекаются, и получаем пустое множество или ничего (в TypeScript есть специальный тип never
, но мы поговорим про него в следующих частях).
Пересекать кортежи не имеет смысла, потому, что мы получим пустое множество значений. Однако у массивов есть один интересный пример. Если мы будем пересекать несколько массивов с разными типами элементов или использовать массив с пересечением простых типов - у нас всегда будет один элемент, который будет удовлетворять всем типам. Это пустой массив []
.
Ранее мы говорили, что любому типу массива принадлежит значение пустого массива. Поэтому в TypeScript такое присваивание вполне валидно. Однако, применение такого пересечения на практике вызывает сомнения.
Допустим, мы хотим объявить переменную, в которую будем записывать значение css свойства . Эта переменная может принимать как числовые, так и строчные значения. (Мы не будем описывать идеально точный тип для этого свойства, а решим модельную задачу в качестве демонстрации).