In an Angular template, what is the difference between user?.name and user!.name, and what does each do when user is null?
answer
- one is a runtime guard
- one exists only for the type checker
- short-circuit versus TypeError
- undefined since v22
- narrowing beats asserting
basics
~20 suser?.name is a runtime guard: if user is null or undefined it short-circuits and yields undefined (null before v22). user!.name is a TypeScript non-null assertion that only silences the type checker; at runtime a null user still throws a TypeError.
solid answer
~40 sThe **safe-navigation operator** `?.` generates a runtime check: when the left side is `null` or `undefined` the rest of the chain is skipped and the expression yields `undefined` since Angular 22 (it yielded `null` before). The **non-null assertion** `!` is TypeScript syntax that Angular templates accept for the template type checker: it tells strict templates "this is not null here" and is erased from the compiled code, so `user!.name` with a `null` user throws `TypeError` during change detection. Use `?.` (often with `??` for a fallback) when the value can really be missing, and `!` only when you know it cannot be but the checker cannot see why. Better still, narrow with `@if (user(); as u)` so neither is needed.
code
ts · 17 linesimport { Component, signal } from '@angular/core';
interface User { name: string }
@Component({
selector: 'app-welcome',
template: `
<p>{{ user()?.name ?? 'guest' }}</p>
<!-- <p>{{ user()!.name }}</p> throws while user() is null -->
@if (user(); as u) {
<p>Signed in as {{ u.name }}</p>
}
`,
})
export class Welcome {
protected readonly user = signal<User | null>(null);
}go deeper
Know that ?. guards against a missing value at runtime while ! only silences the type checker.
Explain the v22 undefined result of ?., why ! is erased, and how @if with as narrows instead.
Review templates for ! used on values that are null during the first render, and prefer narrowing or fallbacks with ??.
Set a lint and review stance on non-null assertions in templates so type safety is not traded for convenience across the codebase.
## Two operators that look alike Both operators sit between an object and a property, and both appear when a value "might be null". They do completely different jobs. | | `user?.name` | `user!.name` | |---|---|---| | Name | safe navigation (optional chaining) | non-null assertion | | Exists at runtime | yes - compiled into a check | no - erased by the compiler | | When `user` is `null` | yields `undefined` (v22+) | throws `TypeError` | | Effect on type checking | result type includes `undefined` | removes `null`/`undefined` from the type | | Typical use | value genuinely optional | value guaranteed but checker cannot prove it | ## The safe-navigation operator `?.` guards a read at **runtime**. If the left side is `null` or `undefined`, evaluation stops and the whole chain produces a nullish result instead of throwing. - **Since Angular 22** that result is `undefined`, the same as JavaScript's optional chaining. **Before v22** Angular returned `null`. Code that compared the result with `=== null` or passed it to a function checking for `null` behaves differently after the upgrade; the `ng update` migration wrapped such uses in `$safeNavigationMigration(...)` to keep the old result. - In **interpolation** the difference is invisible, because `null` and `undefined` both render as an empty string. - It combines naturally with `??`: `{{ user()?.name ?? 'guest' }}` shows a fallback for any missing link. - With strict templates, applying `?.` to a value whose type cannot be `null` or `undefined` triggers the extended diagnostic "Optional chain not nullable", which is a hint that the guard is dead code. ## The non-null assertion `!` comes from **TypeScript**. Angular's template parser accepts it so that templates can talk to the **template type checker** in the same way class code talks to `tsc`: - It narrows the type: `user!.name` is typed as if `user` could not be `null`. - It is **erased** from the generated code; nothing checks the value at runtime. - If the assumption is wrong, the read throws a `TypeError` ("Cannot read properties of null") while Angular checks the view, and the error goes to Angular's error handling instead of the value rendering. A related tool is `$any(expr)`, which casts to `any` for the checker and is likewise erased at runtime. ## How the type checker sees each With strict templates, Angular type-checks every expression against the component's types, so both operators change what the checker infers: 1. `user()?.name` on a `User | null` signal is typed `string | undefined`, so passing it to an input typed `string` is an error until you add a fallback (`?? ''`). 2. `user()!.name` is typed `string`, so the same input binding compiles - which is exactly why the assertion is tempting and why it hides a real `null` state. 3. `$any(user()).name` is typed `any`, turning checking off for that expression entirely. ## When to use which 1. **Value can really be missing** (a signed-out user, data still loading): use `?.`, usually with `??` for display text. 2. **Value is guaranteed** by something the checker cannot see (a route resolver ensured it, a parent only renders the child when set): `!` is acceptable, but document why. 3. **A block of markup depends on the value**: prefer a control-flow block such as `@if (user(); as u) { ... }`, which narrows the type for everything inside and avoids repeating either operator. ## Common mistakes - Using `!` "to make the red squiggle go away" on a value that is `null` during the first render - for example a signal that starts as `null` until an HTTP response arrives. The template then throws on first check. - Believing `!` provides a default value or skips rendering. - Chaining `?.` everywhere on values that can never be nullish, which hides real type errors and makes readers think the data is optional. - Expecting `user?.name` to still be `null` in v22 code that compares with `=== null`.
- Why can user()!.name pass the build yet crash on the first render?The `!` assertion only removes `null` from the type seen by the template type checker, so the build succeeds. It is erased from the compiled template, so if the signal still holds `null` when the view is first checked, reading `.name` throws a `TypeError`.
- When is the optional chain not nullable diagnostic useful?It warns when `?.` is applied to a value whose declared type can never be `null` or `undefined`. That usually means the guard is unnecessary, or that the type is wrong and should include `null`; either way the template is misleading readers about what can be missing.
?. is checking that a mailbox exists before opening it; ! is telling the building inspector not to worry about the mailbox - the inspector moves on, but if there is no mailbox your hand still meets the wall.
saying these in an interview costs you the question
- user!.name returns undefined when user is null
- The ! operator provides a runtime null check in templates
- user?.name still yields null in Angular 22
- ?. and ! compile to the same code
- Adding ! is the right fix for any possibly-null template error