Сужение типов и type guards
Как поток управления сужает union до одного варианта — проверки typeof и instanceof, оператор in, размеченные объединения, пользовательские type guards, assertion-функции и проверка полноты через never.
12 вопросов
JuniorТеорияОчень частоЧто делает union размеченным, и как его сузить?
Что делает union размеченным, и как его сузить?
Каждый вариант объявляет одно поле с разным литеральным типом — дискриминант, например kind: 'circle' против kind: 'square'. Проверка этого поля в if или switch сужает объект целиком до одного варианта, включая уникальные для него поля.
Типичные ошибки
- ✗Типизировать дискриминант как
string, а не литеральным типом, убивая сужение - ✗Считать, что сужение работает в
switch, но не вif - ✗Ждать сужения только дискриминанта, а не всего объекта целиком
Уточняющие вопросы
- →Почему тип
kind: stringполностью выключает сужение union? - →Как заставить компилятор отвергнуть необработанный вариант?
JuniorТеорияОчень частоКак ведут себя ?. и ?? на null против 0 или пустой строки?
Как ведут себя ?. и ?? на null против 0 или пустой строки?
?. замыкается накоротко: если слева null или undefined, всё обращение даёт undefined вместо исключения. ?? подставляет запасное значение только для этих двух случаев — в отличие от || он не трогает 0, '' и false.
Типичные ошибки
- ✗Считать
??синонимом||и терять законные0или пустую строку - ✗Ждать, что
a?.bвычислится вnull, а не вundefined - ✗Считать, что
?.подавляет ошибки, брошенные внутри самого доступа к свойству
Уточняющие вопросы
- →Почему
count ?? 0ведёт себя иначе, чемcount || 0? - →Что сделает
a?.b.c, еслиaопределено, аb—undefined?
JuniorТеорияЧастоКакие проверки заставляют TypeScript сузить union до одного варианта?
Какие проверки заставляют TypeScript сузить union до одного варианта?
Сужение запускают понятные компилятору проверки времени выполнения: typeof, instanceof, in, Array.isArray, проверка на истинность и сравнение с литеральным дискриминантом. Внутри защищённой ветки значение имеет суженный тип; в ветке else остаются те варианты, что не отсеялись.
Типичные ошибки
- ✗Считать, что для сужения нужен
as, а не обычная проверка во время выполнения - ✗Ждать, что
instanceofсузит до interface, у которого нет конструктора в рантайме - ✗Полагать, что ветка else сохраняет весь union, а не оставшиеся варианты
Уточняющие вопросы
- →Почему
instanceofне может сузить значение до interface? - →До чего сужает ветка else проверки
typeof x === 'string'?
MiddleКодЧастоЗаставьте switch по union не компилироваться при добавлении варианта
Заставьте switch по union не компилироваться при добавлении варианта
В ветке default присвойте предмет switch переменной типа never. Когда все case разобраны, предмет сужен до never, и присваивание проходит проверку. Добавьте вариант — остаток перестанет быть never, и присваивание не скомпилируется.
Типичные ошибки
- ✗Обходиться одним
default: throw, который спокойно компилируется при новом варианте - ✗Типизировать дискриминант как
string, из-за чего предмет не сужается доnever - ✗Ждать срабатывания проверки во время выполнения, а не при компиляции
Уточняющие вопросы
- →Почему только
neverзаставляет это присваивание работать? - →Как переиспользовать эту проверку во многих switch, не копируя её?
MiddleТеорияЧастоЧто на самом деле делает non-null assertion ! и чего это стоит?
Что на самом деле делает non-null assertion ! и чего это стоит?
x! убирает null и undefined из статического типа и больше ничего не делает. Он не даёт проверки во время выполнения и стирается, поэтому это обещание, а не доказательство: если значение всё же нулевое, код упадёт на первом обращении к члену, только компилятору велели молчать.
Типичные ошибки
- ✗Считать, что
!вставляет проверку в рантайме, а не просто затыкает компилятор - ✗Ставить
!там, где настоящая проверка или assertion-функция доказали бы утверждение - ✗Полагать, что компилятор подтверждает это утверждение анализом потока управления
Уточняющие вопросы
- →Когда оправдан
!в объявлении поля class о гарантированном присваивании? - →Что вы напишете вместо
!, чтобы сохранить проверку во время выполнения?
MiddleКодЧастоНапишите type predicate, сужающий значение unknown до User
Напишите type predicate, сужающий значение unknown до User
Пользовательский type guard — функция с типом возврата value is User. Когда она возвращает true, компилятор сужает аргумент до User в этой ветке. Тело обязано реально проверять форму в рантайме: предикату доверяют, его не верифицируют.
Типичные ошибки
- ✗Забывать, что
typeof null === 'object', и пропускатьnullчерез проверку - ✗Считать, что компилятор сверяет тело предиката с объявленной формой
- ✗Возвращать
booleanвместоvalue is User, что не даёт никакого сужения
Уточняющие вопросы
- →Что произойдёт с сужением, если тело предиката проверит не то поле?
- →Как валидатор схемы заменил бы этот написанный вручную guard?
JuniorТеорияИногдаКак оператор in сужает union из объектных типов?
Как оператор in сужает union из объектных типов?
'swim' in animal спрашивает во время выполнения, есть ли такой ключ у значения, и компилятор сужает по ответу: в ветке true остаются только варианты с swim, в ветке false — все прочие. Он читает обычную форму объекта, поэтому class не нужен.
Типичные ошибки
- ✗Считать
inзапросомkeyofвремени компиляции, а не проверкой ключа в рантайме - ✗Полагать, что для сужения через
inнужен class или общее поле-дискриминант - ✗Забывать, что ветка false тоже сужает — до вариантов без этого ключа
Уточняющие вопросы
- →Как ведёт себя
in, если ключ объявлен необязательным во всех вариантах? - →Когда стоит взять поле-дискриминант вместо проверки через
in?
MiddleТеорияИногдаЧто даёт asserts x is T такого, чего не даёт type predicate?
Что даёт asserts x is T такого, чего не даёт type predicate?
Assertion-функция сужает через бросок: как только assertIsUser(v) вернула управление, v имеет тип User до конца области видимости, без всякого if. Цель вызова обязана нести явную аннотацию, поэтому выведенная стрелочная const отвергается, а телу доверяют без проверки.
Типичные ошибки
- ✗Присваивать assertion в выведенную
const, что компилятор отвергает сразу - ✗Ждать, что сужение ограничится веткой
if, как у предиката - ✗Полагать, что компилятор проверяет, что тело действительно бросает на плохом значении
Уточняющие вопросы
- →Почему выведенная стрелочная
constне годится как цель вызова assertion? - →Когда вы предпочтёте
assertsбулеву type predicate?
MiddleДизайнИногдаКоллега моделирует состояние хранилища загрузки данных одним interface с четырьмя необязательными полями — isIdle, isLoading, data и error — и различает четыре состояния, читая булевы флаги. На ревью вы предлагаете вместо этого размеченный union по полю status, где каждое состояние несёт только принадлежащие ему данные. Объясните, что в каждом случае может доказать компилятор, какие невозможные комбинации всё ещё допускает форма на флагах, как потребители читают состояние в обоих вариантах и во что обойдётся union при добавлении нового состояния.
Коллега моделирует состояние хранилища загрузки данных одним interface с четырьмя необязательными полями — isIdle, isLoading, data и error — и различает четыре состояния, читая булевы флаги. На ревью вы предлагаете вместо этого размеченный union по полю status, где каждое состояние несёт только принадлежащие ему данные. Объясните, что в каждом случае может доказать компилятор, какие невозможные комбинации всё ещё допускает форма на флагах, как потребители читают состояние в обоих вариантах и во что обойдётся union при добавлении нового состояния.
Опишите каждое состояние отдельным вариантом с литеральным полем status, несущим только свои данные: {status:'loading'}, {status:'success'; data}, {status:'error'; error}. Невозможные комбинации тогда нельзя построить, а потребители сужают тип по status вместо жонглирования флагами.
Типичные ошибки
- ✗Оставлять необязательные
dataиerror, из-за чегоloading + errorостаётся представимым - ✗Полагать, что булевы флаги дают компилятору что-то, по чему можно сузить тип
- ✗Забывать, что новый вариант ломает каждого потребителя с исчерпывающим switch
Уточняющие вопросы
- →Как сохранить предыдущие
dataвидимыми во время повторной загрузки? - →Почему булев флаг — плохой дискриминант даже при двух состояниях?
SeniorКодИногдаТипизируйте reducer так, чтобы необработанный action ломал сборку
Типизируйте reducer так, чтобы необработанный action ломал сборку
Делайте switch по литеральному полю type и завершайте его default: return assertNever(action), где assertNever(value: never): never бросает исключение. Каждый разобранный case отсекает свой вариант: при полном union остаток равен never, а новый action ломает сборку.
Типичные ошибки
- ✗Типизировать параметр
assertNeverкакunknownилиany, что принимает любой action - ✗Возвращать старое состояние из
default, что компилируется вечно и без жалоб - ✗Расширять union полем
typeтипаstring, из-за чего ничто не сужается доnever
Уточняющие вопросы
- →Почему параметр
assertNeverобязан иметь типnever, а неunknown? - →Как это сочетается с union из action, собранным из многих слайсов?
SeniorТеорияРедкоПереживает ли сужение вызов функции, который мог переприсвоить значение?
Переживает ли сужение вызов функции, который мог переприсвоить значение?
Переживает — и это намеренная несостоятельность. Компилятор сбрасывает сужение только при присваивании, которое видит в том же потоке, поэтому вызов, переприсвоивший захваченную let или изменивший obj.prop, оставляет устаревшее сужение в силе. А вот сужение, читаемое внутри колбэка, он сбрасывает.
Типичные ошибки
- ✗Считать, что произвольный вызов расширяет суженное свойство обратно до объявленного типа
- ✗Полагать, что сужение всегда проникает в тело колбэка
- ✗Доверять суженному
obj.propпосле передачиobjв код, который может его изменить
Уточняющие вопросы
- →Почему копирование
obj.propвconstсохраняет сужение внутри колбэка? - →Какое присваивание компилятор должен увидеть, чтобы сбросить сужение?
SeniorДизайнРедкоВы поддерживаете общий пакет, экспортирующий размеченный union Event из трёх вариантов, который используют десяток приложений. Продуктовая задача требует четвёртый вариант. Один ревьюер говорит, что union — открытое множество, поэтому добавление варианта аддитивно и безопасно; другой утверждает, что это ломает каждого потребителя и требует мажорной версии. Объясните, кто прав для кода, который создаёт Event, и для кода, который его разбирает, во что превращает это изменение паттерн проверки полноты и как вы выпустите четвёртый вариант, не сломав молча приложения-потребители.
Вы поддерживаете общий пакет, экспортирующий размеченный union Event из трёх вариантов, который используют десяток приложений. Продуктовая задача требует четвёртый вариант. Один ревьюер говорит, что union — открытое множество, поэтому добавление варианта аддитивно и безопасно; другой утверждает, что это ломает каждого потребителя и требует мажорной версии. Объясните, кто прав для кода, который создаёт Event, и для кода, который его разбирает, во что превращает это изменение паттерн проверки полноты и как вы выпустите четвёртый вариант, не сломав молча приложения-потребители.
Оба, но с разных сторон. Для кода, который создаёт Event, union просто расширился, и старые производители проходят проверку. А у кода, который его разбирает, в каждом исчерпывающем switch появился необработанный вариант, и проверка через never даёт ошибку компиляции. Это ломающее изменение.
Типичные ошибки
- ✗Называть изменение аддитивным лишь потому, что сам union только вырос
- ✗Считать, что ветка
defaultзащищает исчерпывающий switch от нового варианта - ✗Путать сторону производителя, где расширение безопасно, со стороной потребителя
Уточняющие вопросы
- →Как отдельный тип
EventV2позволит потребителям мигрировать в своём темпе? - →Почему ветка
defaultослабляет гарантию, а не усиливает её?