Сужение типов
-
Защитник типа
typeof
- Проверка на истинность (truthiness narrowing)
- Проверка на равенство (equality narrowing)
-
Сужение типов с помощью оператора
instanceof
- Присвоения (assignments)
- Анализ потока управления (control flow analysis)
- Использование предикатов типа (type predicates)
- Исключающие объединения (discriminated unions)
-
Тип
never
Предположим, что у нас имеется функция под названием padLeft
:
Если padding
- это number
, значит, мы хотим добавить указанное количество пробелов перед input
. Если padding
- это string
, значит, мы просто хотим добавить padding
перед input
. Попробуем реализовать логику, когда padLeft
принимает number
для padding
:
Мы получаем ошибку. TS
предупреждает нас о том, что добавление number
к number | string
может привести к неожиданному результату, и он прав. Другими словами, мы должны проверить тип padding
перед выполнением каких-либо операций с ним:
Выражение typeof padding === 'number'
называется защитником или предохранителем типа (type guard). А процесс приведения определенного типа к более конкретной версии с помощью защитников типа и присвоений называется сужением типа (narrowing).
Для сужения типов может использоваться несколько конструкций.
typeof
#
Защитник типа Оператор typeof
возвращает одну из следующих строк:
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
Рассмотрим интересный пример:
В функции printAll
мы пытаемся проверить, является ли переменная strs
объектом (массивом). Но, поскольку выражение typeof null
возвращает object
(по историческим причинам), мы получаем ошибку.
Таким образом, в приведенном примере мы выполнили сужение к string[] | null
вместо желаемого string[]
.
#
Проверка на истинность (truthiness narrowing)В JS
мы можем использовать любые выражения в условиях, инструкциях &&
, ||
, if
, приведении к логическому значению с помощью !
и т.д. Например, в инструкции if
условие не всегда должно иметь тип boolean
:
В JS
конструкции типа if
преобразуют условия в логические значения и выбирают ветку (с кодом для выполнения) в зависимости от результата (true
или false
). Значения
- 0
- NaN
- "" (пустая строка)
- 0n (bigint-версия нуля)
- null
- undefined
являются ложными, т.е. преобразуются в false
, остальные значения являются истинными, т.е. преобразуются в true
. Явно преобразовать значение в логическое можно с помощью функции Boolean
или с помощью двойного отрицания (!!
):
Данная техника широко применяется для исключения значений null
и undefined
. Применим ее к нашей функции printAll
:
Теперь ошибки не возникает, поскольку мы проверяем, что strs
является истинным. Это защищает нас от таких ошибок как:
Обратите внимание
Проверка примитивов на истинность также подвержена подобным ошибкам. Рассмотрим другую реализацию printAll
:
Мы обернули тело функции в проверку на истинность, но у такого подхода имеется один существенный недостаток: мы больше не можем корректно обрабатывать случай передачи пустой строки в качестве аргумента.
Напоследок, рассмотрим пример использования логического оператора "НЕ":
#
Проверка на равенство (equality narrowing)Для сужения типов также можно воспользоваться инструкцией switch
или операторами равенства ===
, !==
, ==
, !=
, например:
Когда мы сравниваем значения x
и y
, TS
знает, что их типы также должны быть равны. Поскольку string
- это единственный общий тип, которым обладают и x
, и y
, TS
знает, что x
и y
должны быть string
в первой ветке.
Последняя версия нашей функции printAll
была подвержена ошибкам, поскольку мы некорректно обрабатывали случай получения пустой строки. Перепишем ее с использованием оператора равенства:
Операторы абстрактного равенства (==
и !=
) также могут использоваться для сужения типов, в некоторых случаях их использование даже более эффективно, чем использование операторов строгого равенства (===
и !==
). Например, выражение == null
проверяет на равенство не только с null
, но и с undefined
. Аналогично выражение == undefined
проверяет на равенство не только с undefined
, но и с null
.
in
#
Сужение типов с помощью оператора В JS
существует оператор для определения наличия указанного свойства в объекте - оператор in
. TS
позволяет использовать данный оператор для сужения поетнциальных типов.
Например, в выражении 'value' in x
, где 'value' - строка, а x
- объединение, истинная ветка сужает типы x
к типам, которые имеют опциональное или обязательное свойство value
, а ложная ветка сужает типы к типам, которые имеют опциональное или не имеют названного свойства:
Наличие опциональных свойств в обоих ветках не является ошибкой, поскольку человек, например, может и плавать (swim), и летать (fly) (при наличии соответствующего снаряжения):
instanceof
#
Сужение типов с помощью оператора Оператор instanceof
используется для определения того, является ли одна сущность "экземпляром" другой. Например, выражение x instanceof Foo
проверяет, содержится ли Foo.prototype
в цепочке прототипов x
. Данный оператор применяется к значениям, сконструированным с помощью ключевого слова new
. Он также может использоваться для сужения типов:
#
Присвоения (assignments)Как упоминалось ранее, когда мы присваиваем значение переменной, TS
"смотрит" на правую часть выражения и вычисляет тип для левой части:
Данные присвоения являются валидными, поскольку типом, определенным для x
, является string | number
. Однако, если мы попытаемся присвоить x
логическое значение, то получим ошибку:
#
Анализ потока управления (control flow analysis)Анализ потока управления - это анализ, выполняемый TS
на основе достижимости кода (reachability) и используемый им для сужения типов с учетом защитников типа и присвоений. При анализе переменной поток управления может разделяться и объединяться снова и снова, поэтому переменная может иметь разные типы в разных участках кода:
#
Использование предикатов типа (type predicates)Иногда мы хотим иметь более прямой контроль над тем, как изменяются типы. Для определения пользовательского защитника типа необходимо определить функцию, возвращаемым значением которой является предикат типа:
pet is Fish
- это наш предикат. Предикат имеет форму parameterName is Type
, где parameterName
- это название параметра из сигнатуры текущей функции.
При вызове isFish
с любой переменной, TS
"сузит" эту переменную до указанного типа, разумеется, при условии, что оригинальный тип совместим с указанным.
Обратите внимание
TS
знает не только то, что pet
- это Fish
в ветке if
, но также то, что в ветке else
pet
- это Bird
.
Мы можем использовать защитника isFish
для фильтрации массива Fish | Bird
и получения массива Fish
:
#
Исключающие объединения (discriminated unions)Предположим, что мы пытаемся закодировать фигуры, такие как круги и квадраты. Круги "следят" за радиусом, а квадраты - за длиной стороны. Для обозначения того, с какой фигурой мы имеем дело, будет использоваться свойство kind
. Вот наша первая попытка определить Shape
:
Использование 'circle' | 'square'
вместо string
позволяет избежать орфографических ошибок:
Давайте создадим функцию getArea
для вычисления площади фигур. Начнем с кругов:
С включенной настройкой strictNullChecks
мы получаем ошибку, поскольку radius
может быть не определен. Что если перед выполнением кода проверить свойство kind
?
Хм, TS
по-прежнему не понимает, что с этим делать. В данном случае, мы знаем больше, чем компилятор. Можно попробовать использовать ненулевое утверждение (!
после shape.radius
), чтобы сообщать компилятору о том, что radius
точно присутствует в типе:
Код компилируется без ошибок, но решение не выглядит идеальным. Мы, определенно, можем сделать его лучше. Проблема состоит в том, что компилятор не может определить, имеется ли свойство radius
или sideLength
на основе свойства kind
. Перепишем определение Shape
:
Мы разделили Shape
на два разных типа с разными значениями свойства kind
, свойства radius
и sideLength
являются обязательными в соответствующих типах.
Посмотрим, что произойдет при попытке получить доступ к свойству radius
типа Shape
:
Мы получаем ошибку. На этот раз TS
сообщает нам о том, что shape
может быть Square
, у которого нет radius
. Что если мы снова попытаемся выполнить проверку свойства kind
?
Код компилируется без ошибок. Когда каждый тип объединения содержит общее свойства с литеральным типом, TS
рассматривает это как исключающее объединение и может сужать членов данного объединения.
В нашем случае, общим свойством является kind
(которое рассматривается как особое свойство Shape
). Проверка значения этого свойства позволяет сужать shape
до определенного типа. Другими словами, если значением kind
является 'circle'
, shape
сужается до Circle
.
Тоже самое справедливо и в отношении инструкции switch
. Теперь мы можем реализовать нашу функцию getArea
без !
:
never
#
Тип Для представления состояния, которого не должно существовать, в TS
используется тип never
.
#
Исчерпывающие проверки (exhaustiveness checking)Тип never
может быть присвоен любому типу; однако, никакой тип не может быть присвоен never
(кроме самого never
). Это означает, что never
можно использовать для выполнения исчерпывающих проверок в инструкции switch
.
Например, добавим такой default
в getArea
:
После этого попытка добавления нового члена в объединение Shape
будет приводить к ошибке: