Пусто: null, undefined и строгие проверки
- Зачем это нужно
- Две пустоты: одна возникает сама, другую кладут руками
- Условие принимает что угодно — и это ловушка
- Строгий режим: один флаг меняет язык
- Сужение типа: как убедить компилятора
- Обращение, которое не падает
- Массив не защищён даже в строгом режиме
- Как читать самую частую ошибку
- Цена восклицательного знака
- Две пустоты по-разному уезжают наружу
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Пятнадцать глав подряд язык гарантировал: значение есть всегда. Несуществующий ключ словаря отдавал нули, нулевой адрес, структуру из нулей — поэтому в контракте и заводили отдельное поле-флаг «а записан ли пользователь вообще».
Здесь всё иначе. Значения может не быть, и это отдельное состояние — причём в двух разных обличьях. А самая частая ошибка выполнения в этом языке звучит так: Cannot read properties of undefined. Эта глава — про то, откуда она берётся и как сделать так, чтобы её ловил компилятор, а не пользователь.
Две пустоты: одна возникает сама, другую кладут руками
undefined — «сюда не клали». Никто его не пишет; оно возникает само в четырёх случаях:
- переменная объявлена без значения;
- у объекта нет такого поля;
- функция ничего не вернула;
- элемента массива не существует.
null — «положили пусто». Само не появляется никогда: его пишет человек или присылает внешняя система, и означает оно осознанное «здесь ничего».
Проверка типа у null отвечает "object" — это ошибка языка, которой тридцать лет, и чинить её не станут: слишком много кода на неё опирается. Поэтому null проверяют строгим сравнением, а не проверкой типа.
Условие принимает что угодно — и это ловушка
В Solidity if принимал только логическое значение. Здесь в условие пускают любое, и «ложными» оказываются восемь значений: false, ноль, минус ноль, ноль в больших числах, пустая строка, null, undefined и «не число».
Значит, проверка «если значение есть» отвергает настоящий нулевой баланс и пустую строку. Для протокола, где ноль — законная сумма, это прямая ошибка в логике.
Первая модель прогоняет четырнадцать значений через четыре проверки:
значение typeof if(x) x ?? def x || def x?.length
-----------------------------------------------------------
0 number нет 0 'def' undefined
-0 number нет -0 'def' undefined
0n bigint нет 0n 'def' undefined
'' string нет '' 'def' 0
'0' string да '0' '0' 1
' ' string да ' ' ' ' 1
NaN number нет NaN 'def' undefined
false boolean нет false 'def' undefined
true boolean да true true undefined
null object нет 'def' 'def' undefined
undefined undefined нет 'def' 'def' undefined
[] object да [] [] 0
{} object да {} {} undefined
42 number да 42 42 undefined
|| и ?? разошлись на 6 значениях: 0, -0, 0n, '', NaN, false
typeof null === "object": true
null == undefined: true, null === undefined: false
Три строки вывода стоят отдельного внимания.
Пустой массив и пустой объект — истинны. Проверка «если массив есть» не отвечает на вопрос «есть ли в нём что-нибудь». Для этого смотрят длину.
Строка «0» истинна, а число 0 — нет. Отсюда классическая ошибка при разборе данных, пришедших текстом.
|| и ?? расходятся на шести значениях. Первый подставляет запасное значение при любом «ложном», второй — только при настоящей пустоте. Для суммы, ставки, процента нужен именно второй: иначе законный ноль молча превратится в значение по умолчанию.
Строгий режим: один флаг меняет язык
Без строгой проверки пустоты типы «строка» и «строка или пусто» неотличимы: компилятор считает пустоту допустимым значением любого типа и молчит. С флагом — пустота становится частью типа, и обращаться к ней без проверки нельзя.
Вторая модель компилирует один и тот же файл дважды:
── без строгой проверки: компилятор промолчал, программа запущена ──
упало: TypeError: Cannot read properties of undefined (reading 'height')
упало: TypeError: Cannot read properties of undefined (reading 'generator')
упало: TypeError: Cannot read properties of undefined (reading 'length')
упало: TypeError: Cannot read properties of undefined (reading 'height')
blocks[10] сам по себе: undefined
blocks.length = 2
── с --strict: до запуска дело не дошло ──
model2.ts(20,37): error TS18048: 'found' is possibly 'undefined'.
model2.ts(27,29): error TS2532: Object is possibly 'undefined'.
model2.ts(35,42): error TS18048: 'g' is possibly 'undefined'.
Четыре падения превратились в три сообщения компилятора — до запуска. Это и есть ответ на вопрос «зачем вообще нужен TypeScript, если Node прекрасно запускает .js».
Сужение типа: как убедить компилятора
Компилятор читает условия и после них сам убирает пустоту из типа — это называется сужением.
const found = findBlock(height); // тип: Block | undefined
if (found === undefined) return "не найден";
return found.generator; // здесь тип уже Block
Работают четыре формы: сравнение с пустотой, проверка типа, проверка наличия поля и проверка «это массив». Не работает одна — та самая проверка «если значение есть»: она отсекает заодно и ноль с пустой строкой.
Обращение, которое не падает
Знак ?. возвращает пустоту вместо падения, если слева пусто:
const gen = block?.generator; // undefined, если block пуст
const first = list?.[0]; // то же для элемента
const res = handler?.(); // и для вызова
Вся цепочка обрывается сразу: если пусто в начале, остальное не вычисляется вовсе.
Это удобно для чтения и опасно для записи: ?. превращает падение в тихий undefined, который поедет дальше по программе и всплывёт в другом месте. Правило: ?. — там, где пустота ожидаема и обработана; проверка — там, где пустота означает ошибку.
Массив не защищён даже в строгом режиме
В Solidity выход за границу массива давал панику и откат всей транзакции. Здесь — тихую пустоту:
blocks[10] сам по себе: undefined
blocks.length = 2
И тип, который выведет компилятор, будет «блок», а не «блок или пусто»: по умолчанию язык считает, что вы знаете, что делаете. Отдельная настройка это меняет, но в стандартный строгий набор она не входит — потому что обычный цикл превращается в мешанину из проверок.
Практический вывод: после обращения по индексу проверять результат, если индекс пришёл не из длины того же массива.
Как читать самую частую ошибку
TypeError: Cannot read properties of undefined (reading 'height')
Читается по одному правилу: в скобках — имя того, что пытались прочитать, а undefined — это то, что стояло слева от точки. То есть виновата не height, а выражение перед ней.
Дальше по трассировке видно строку и файл. Три обычные причины: не нашли элемент в массиве, обратились к полю объекта, которого нет в ответе, или забыли, что функция может ничего не вернуть.
Цена восклицательного знака
Знак ! после выражения означает «я знаю, что здесь не пусто» — и убирает пустоту из типа без всякой проверки. Третья модель ставит рядом три способа:
(tsc --strict: ошибок нет)
--- пользователь существует ---
withCheck ('lender1') -> lender1 / 0xA1 / 1000
withOptional('lender1') -> 0xA1
withBang ('lender1') -> lender1 / 0xA1 / 1000
--- пользователя нет ---
withCheck ('ghost') -> нет такого пользователя
withOptional('ghost') -> адрес неизвестен
withBang ('ghost') -> УПАЛО: TypeError: Cannot read properties of undefined (reading 'login')
--- что осталось от `!` в рантайме ---
текст функции withBang: const u = findUser(login) ;
Последняя строка объясняет всё: в готовом коде от восклицательного знака не остаётся ничего. Это обещание компилятору, а не проверка. Компилятор поверил на слово и замолчал — а пользователь получил падение.
Правило: ! уместен там, где невозможность пустоты видна из соседних строк и не зависит от внешних данных. Во всех остальных случаях — проверка.
Две пустоты по-разному уезжают наружу
При превращении объекта в текст для передачи разница перестаёт быть теоретической: поле с undefined исчезает целиком, а поле с null остаётся со значением «пусто».
Отсюда практическое следствие для скриптов, которые разговаривают с узлом: если сервис ждёт поле и получает объект без него — это не то же самое, что получить поле со значением «пусто». Первое обычно означает «параметр не передан», второе — «передан и он пустой».
Типичные ошибки
|| значение_по_умолчанию для суммы. Законный ноль превращается в значение по умолчанию.
Проверка «если значение есть» вместо сравнения с пустотой. Отсекает ноль и пустую строку заодно.
Проверка типа для null. Отвечает "object" — сравнивать нужно строго.
Обращение по индексу без проверки. Даже в строгом режиме компилятор считает элемент существующим.
?. там, где пустота означает ошибку. Падение превращается в тихую пустоту, которая всплывёт позже и в другом месте.
! вместо проверки. Компилятор молчит, программа падает.
Необязательное поле, к которому обращаются напрямую. Компилятор укажет: значение может быть пустым.
== вместо === при сравнении с пустотой. Нестрогое сравнение считает две разные пустоты равными — иногда это удобно, но должно быть осознанным.
Пустота, приехавшая из ответа узла. Поля, которого нет в ответе, не существует — и типизировать ответ как «точно есть» значит обмануть себя.
Слова главы
undefined— «не клали»; возникает само в четырёх случаях.null— «положили пусто»; пишется руками.- «Ложные» значения — восемь:
false,0,-0,0n, пустая строка,null,undefined, «не число». - Строгая проверка пустоты — флаг компилятора, делающий пустоту частью типа.
- Сужение типа — компилятор убирает пустоту после проверки.
?.— обращение, возвращающее пустоту вместо падения.??— запасное значение только вместо настоящей пустоты.?в типе — необязательное поле или параметр.!— «я знаю, что не пусто»; в готовом коде исчезает.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
if (found === undefined) return "блок не найден";
На чемпионате
Модуль А. Дашборд из задания показывает больше десятка метрик: общий объём средств, статистика ликвидаций, текущее отношение долга к залогу каждой позиции, накопленные награды, цена доли. Каждая метрика — величина, которой может не быть: позиций ноль, ликвидаций не было, доход не начислялся.
Практические выводы:
- «ноль» и «нет данных» — разные состояния, и путать их нельзя: на демонстрации эксперт увидит прочерк там, где должен быть ноль;
- запасное значение подставляют оператором
??, иначе законный нулевой баланс исчезнет; - ответ узла типизируют как «может не быть» — поле, которого нет в ответе, не существует.
Модуль В. Хотя аудит идёт по контракту, привычка думать о пустоте пригодится и там: в Solidity её роль играют нули, и отличить «не записано» от «записано нулём» можно только своим полем-флагом — ровно та же задача, другими средствами.
Что дальше
Пустота разобрана. Следующая глава — про ветвления и циклы: та же тема всплывает под другим углом, потому что условие здесь принимает любое значение, а не только логическое, как требовал if в Solidity.
Углубиться
- Почему проверку типа у
nullне чинят — и что предлагали взамен. - Строгая проверка индексов — отдельная настройка: что она даёт и почему её не включили в стандартный набор.
- Утверждения типа — другие способы сказать компилятору «я знаю лучше» и чем они опасны.
- Функции-предикаты — как написать свою проверку, после которой компилятор сузит тип.
- Пустота в чужих данных — приёмы разбора ответа, о которых пойдёт речь в главе про свои типы.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- TypeScript Handbook: Everyday Types — объединения типов и работа с пустотой.
- TypeScript Handbook: Narrowing — все формы сужения типа.
- tsconfig: strictNullChecks — что именно меняет флаг.
- MDN: nullish coalescing —
??и чем он отличается от||. - MDN: optional chaining —
?.во всех трёх формах.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.