Почему следует быть осторожным с оператором `!` в TypeScript
Возможности TypeScript по обеспечению безопасности работы с null являются одним из его главных преимуществ. При включенной опции strictNullChecks компилятор заставляет вас обрабатывать случаи, когда значение может быть null или undefined, прежде чем его использовать. Но есть один символ, который позволяет обойти всё это: !.
К оператору ненулевого утверждения легко прибегнуть, когда компилятор начинает жаловаться. Понимание того, что он на самом деле делает — и чего не делает — убережёт вас от ошибок, которые действительно сложно отследить.
Ключевые выводы
- Оператор
!(non-null assertion) сообщает компилятору TypeScript, что значение следует рассматривать как ненулевое, но не генерирует никаких проверок во время выполнения — он полностью удаляется из результирующего JavaScript-кода. - Чрезмерное использование
!подрывает саму цельstrictNullChecks, превращая ошибки времени компиляции в сбои времени выполнения. - Более безопасные альтернативы, такие как явные проверки на null, опциональная цепочка (
?.), оператор нулевого слияния (??) и пользовательские assertion-функции, должны быть вашим первым выбором. - Используйте
!только в случаях, когда у вас есть достоверные внешние знания, которые компилятор не может проверить, и рассматривайте каждый такой случай как флаг для code review.
Что на самом деле делает оператор ! в TypeScript
Когда вы добавляете ! к выражению, вы говорите компилятору TypeScript: «Поверь мне, это значение не является ни null, ни undefined». Компилятор удаляет null и undefined из типа и перестаёт жаловаться.
// С включенной опцией strictNullChecks
const input = document.querySelector('input') // тип: HTMLInputElement | null
const value = input!.value // тип: string — компилятор удовлетворён
Вот критически важный момент: ! полностью удаляется из результирующего JavaScript-кода. Никаких проверок во время выполнения. Никакой защиты. Никакой страховочной сетки. Если input на самом деле окажется null во время выполнения, вы получите ту же ошибку Cannot read properties of null, которую пытались избежать в первую очередь.
Утверждение определённого присваивания vs. Утверждение ненулевого значения
Оператор ! появляется в двух различных контекстах, и стоит их различать.
Утверждение ненулевого значения (non-null assertion) — используется в выражениях для удаления null | undefined из типа:
const el = document.getElementById('app')! // HTMLElement, а не HTMLElement | null
Утверждение определённого присваивания (definite assignment assertion) — используется в полях класса или объявлениях переменных, чтобы сообщить компилятору, что свойство будет присвоено до использования, даже если он не может это проверить:
class UserService {
user!: User // "Обещаю, что это будет присвоено до чтения"
}
Оба являются утверждениями времени компиляции, которые заставляют компилятор замолчать без добавления какой-либо защиты во время выполнения. Утверждение ненулевого значения удаляет null | undefined из типа выражения, в то время как утверждение определённого присваивания отключает проверки определённого присваивания для переменной или поля класса.
Почему чрезмерное использование ! подрывает безопасность работы с null в TypeScript
Оператор ! — это аварийный выход, а не решение. Когда вы его используете, вы не исправляете проблему с nullable-значениями — вы скрываете её от компилятора, оставляя полностью нетронутой во время выполнения.
Распространённый паттерн, который вызывает реальные ошибки:
// Опасно: предполагается, что элемент всегда существует
const button = document.querySelector('.submit-btn')!
button.addEventListener('click', handleSubmit)
Если этот элемент не существует в определённом контексте — на другой странице, при условном рендеринге, в тестовом окружении — вы получите сбой во время выполнения. Компилятор не выдал предупреждения, потому что вы сказали ему этого не делать.
Discover how at OpenReplay.com.
Более безопасные альтернативы, которые стоит использовать в первую очередь
Современный TypeScript обладает мощным анализом потока управления. Во многих ситуациях, где разработчики исторически прибегали к !, компилятор теперь может автоматически сужать типы с помощью простой проверки.
Явная проверка на null:
const button = document.querySelector('.submit-btn')
if (button) {
button.addEventListener('click', handleSubmit)
}
Опциональная цепочка с оператором нулевого слияния:
const label = document.querySelector('label')?.textContent ?? 'Default'
Функция type guard:
function assertExists<T>(
val: T | null | undefined,
msg: string
): asserts val is T {
if (val == null) throw new Error(msg)
}
const el = document.getElementById('app')
assertExists(el, 'App element not found')
el.style.display = 'block' // сужено до HTMLElement
Подход с assertion-функцией особенно полезен, потому что он сохраняет гарантию безопасности — если значение null, вы получите явную, описательную ошибку в точке сбоя, а не загадочный сбой где-то дальше по коду.
Когда ! действительно уместен
Есть законные случаи использования. Если у вас есть внешние знания, которые компилятор не может вывести — например, значение, гарантированное жизненным циклом инициализации, который TypeScript не может отследить — ! является разумным инструментом. Ключевой вопрос: действительно ли вы знаете что-то, чего не знает компилятор, или вы просто заглушаете предупреждение?
Если это последнее, предупреждение, вероятно, правильное.
Заключение
Оператор ! в TypeScript не делает ваш код безопаснее — он делает его тише. При неосторожном использовании он превращает ошибки времени компиляции в сбои времени выполнения, что противоположно тому, для чего предназначен strictNullChecks. Сначала обращайтесь к сужению через поток управления, опциональной цепочке или явным проверкам. Оставьте ! для случаев, когда у вас есть подлинная уверенность, которую компилятор не может проверить, и рассматривайте каждый такой случай как флаг для code review, заслуживающий внимания.
Часто задаваемые вопросы
Нет. Восклицательный знак полностью удаляется во время компиляции. Результирующий JavaScript не содержит никаких проверок на null или защитных механизмов. Если значение окажется null или undefined во время выполнения, ваш код выдаст ошибку точно так же, как если бы вы вообще не использовали TypeScript.
Это может быть опасно. Когда вы пишете свойство вроде user!: User, вы сообщаете компилятору, что значение будет присвоено до того, как какой-либо код его прочитает. Если это предположение неверно — например, если ожидаемый шаг инициализации пропущен — обращение к свойству вернёт undefined и, вероятно, вызовет ошибку времени выполнения без предупреждения времени компиляции.
Это разумно, когда у вас есть внешние знания, которые компилятор не может проверить. Распространённый пример — когда значение гарантируется жизненным циклом инициализации, который TypeScript не может отследить (например, хуки инициализации фреймворка или внедрение зависимостей). Даже в этом случае рассмотрите возможность использования явной assertion-функции, чтобы отсутствующее значение вызывало понятное сообщение об ошибке, а не загадочный сбой.
Лучшая альтернатива зависит от контекста. Для поиска элементов DOM и опциональных данных используйте опциональную цепочку с оператором нулевого слияния. Для случаев, когда отсутствующее значение является настоящей ошибкой, используйте пользовательскую assertion-функцию, которая выбрасывает описательное сообщение. Оба подхода сохраняют сужение типов, добавляя при этом реальную безопасность во время выполнения.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.