Восклицательный знак после выражения, value!, это оператор утверждения non-null. Он убирает null и undefined из типа, так что number | undefined можно использовать как number. Это обещание компилятору, а не проверка: во время выполнения ничего не происходит.
Без ! вызов tea.toFixed(2) был бы ошибкой TS18048, 'tea' is possibly 'undefined'. С ним код компилируется, потому что вы сказали компилятору, что ключ существует.
Что x! делает во время выполнения: ничего
! стирается из вывода. Скомпилированный JavaScript для prices.get("coffee")! это просто prices.get("coffee"). Если утверждение неверно, ошибка появляется позже, в первом месте, где используется отсутствующее значение:
Программа печатает Cannot read properties of undefined (reading 'toFixed'). Падение происходит на строке после !, а в реальном коде оно может случиться гораздо дальше: undefined может сохраниться в объекте и взорваться в другом файле. Именно это расстояние делает ! опасным.
Явное присваивание: let x!: T
Тот же символ в объявлении означает нечто родственное. TypeScript отслеживает, присвоено ли переменной значение до чтения, и не может проследить за присваиванием внутри другой функции:
index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.
let config!: { port: number }; это утверждение явного присваивания: «значение будет присвоено до любого чтения». Оно исправляет ошибку с той же оговоркой, что и x!: если init() когда-нибудь пропустят, чтение получит undefined. Обычно лучше перестроить код, например const config = init();, где init возвращает объект.
Свойства классов: prop!: T
При strictPropertyInitialization (входит в strict) каждое свойство класса должно быть инициализировано в объявлении или в конструкторе. Иначе вы получите TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. Если свойство задаётся позже методом или фреймворком, prop!: T говорит компилятору принять это:
Первый console.log показывает пробел: тип говорит, что socket есть всегда, но до open() он равен undefined. Вызов c.socket.send в этот момент компилируется и выбрасывает ошибку. Если свойство действительно может отсутствовать, объявите его как socket?: ... и проверяйте или создавайте объект в конструкторе. Главное место, где prop! это стандартная практика, это фреймворк, который заполняет свойство после создания объекта: @ViewChild(...) child!: ChildDirective в Angular (задаётся до запуска ngAfterViewInit) или классы сущностей ORM, колонки которых библиотека заполняет при загрузке строки (в документации MikroORM пишут @Property() title!: string).
Безопасные альтернативы
Большинство ! можно заменить тем, что проверяет компилятор, или проверкой, которая громко падает в нужном месте:
Обе вспомогательные функции сужают тип так же, как !, но неверное предположение даёт missing HOST прямо на месте, а не TypeError где-то ещё. assertDefined это функция-утверждение (asserts value is ...): после вызова компилятор считает host строкой string. Другие приёмы в разделе защитники типов.
| Вместо | Пишите | Что происходит, если значения нет |
|---|---|---|
user!.name | if (user) { user.name } | блок пропускается |
user!.name | user?.name | undefined |
count! | count ?? 0 | используется значение по умолчанию |
map.get(k)! | must(map.get(k), "k") | понятная ошибка в этой строке |
let x!: T | const x = compute() | ломаться нечему |
Другие восклицательные знаки
! означает разное в зависимости от того, где он стоит:
| Код | Значение |
|---|---|
value! (после выражения) | утверждение non-null, только TypeScript |
let x!: T, prop!: T | утверждение явного присваивания, только TypeScript |
!value (перед выражением) | логическое НЕ, обычный JavaScript |
!!value | преобразование в boolean, обычный JavaScript |
a !== b, a != b | неравенство, обычный JavaScript |
Только первые два стираются при компиляции. !value и !!value выполняются во время работы программы и возвращают boolean.
Часто задаваемые вопросы
Что означает восклицательный знак после переменной в TypeScript?
value! это оператор утверждения non-null. Он сообщает компилятору, что value не null и не undefined, поэтому из его типа исчезают эти два члена: string | undefined становится string. Он удаляется из сгенерированного JavaScript и не добавляет проверки во время выполнения, поэтому если вы ошиблись, программа позже упадёт с TypeError.
Чем ! отличается от ? в TypeScript?
x! утверждает, что значение есть, и даёт вам тип без null, без всякой проверки. x?.y проверяет во время выполнения: если x равно null или undefined, вычисление останавливается и возвращается undefined. В объявлении name?: string делает свойство необязательным, а name!: string говорит, что обязательному свойству будет присвоено значение там, где компилятор этого не видит.
Что означает let x!: string?
Это утверждение явного присваивания (definite assignment assertion). Оно сообщает компилятору, что переменной будет присвоено значение до чтения, хотя компилятор не может этого доказать (например, присваивание происходит внутри другой функции). Без него чтение переменной это ошибка TS2454, Variable 'x' is used before being assigned.
Как исправить "has no initializer and is not definitely assigned in the constructor"?
Это ошибка TS2564 из strictPropertyInitialization. Задайте свойству начальное значение, присвойте его в конструкторе, сделайте свойство необязательным (prop?: T) или, если фреймворк или метод инициализации действительно задаёт его до использования, напишите prop!: T. ! это последний вариант, потому что это обещание никто не проверяет.
Оператор non-null assertion это плохая практика?
Он не ошибочен, но каждый ! это непроверенное утверждение. Конфигурации линтера вроде @typescript-eslint/no-non-null-assertion его отмечают. Предпочитайте проверку, которая сужает тип (if (x), x ?? fallback, x?.y), или вспомогательную функцию, которая выбрасывает понятную ошибку. Оставляйте ! для мест, где значение гарантировано логикой, за которой компилятор не может проследить.