Optional Chaining and Non-null Assertion
- #typescript
- #optional-chaining
- #non-null-assertion
- #undefined
In questa lezione
1. The problem: values that might be undefined
Many operations on arrays and objects can legitimately produce undefined. A classic example is Array.prototype.pop(): if the array is empty, there’s nothing to return, so pop() returns undefined. TypeScript reflects this in its types, and that has consequences for any code chained after it.
const arr = [{ name: "tim" }, { name: "joe" }, { name: "jane" }];
// const el = arr.pop().name; // error: Object is possibly 'undefined'
The compiler is right to complain: arr.pop() has type { name: string } | undefined, and undefined has no .name property. Accessing .name directly would compile in plain JavaScript and simply throw TypeError: Cannot read properties of undefined at runtime if the array happened to be empty. TypeScript catches that possibility before you ever run the code.
2. Optional chaining (?.)
The optional chaining operator ?. solves this cleanly: it accesses a property only if the value on its left is not null/undefined, and short-circuits to undefined otherwise.
const el = arr.pop()?.name;
console.log(el); // el has type: string | undefined
Read ?. as “if the thing on the left exists, continue; otherwise stop here and produce undefined”. The type of el correctly reflects both possible outcomes: string (a real name) or undefined (the array was empty).
?. also works for calling functions that might not exist, and for indexing into arrays/objects that might not exist:
obj.method?.(); // calls method only if it exists, otherwise evaluates to undefined
config?.settings?.theme; // safely drills into a possibly-missing nested structure
list?.[0]; // indexes only if list is not null/undefined
Suggerimento
Optional chaining can be repeated as many times as needed in a single expression: a?.b?.c?.d. As soon as any link in the chain is null/undefined, the whole expression short-circuits to undefined without throwing.
3. Non-null assertion (!)
The non-null assertion operator ! tells the compiler “ignore the possibility that this is null or undefined — I know better”. It doesn’t check anything at runtime; it purely silences the compiler.
const arr2 = [[{ name: "tim" }]];
const el2 = arr2.pop()!.pop()!.name;
console.log(el2); // string
Here, arr2.pop() returns { name: string }[] | undefined. The first ! tells TypeScript “trust me, this popped value is not undefined”, letting you call .pop() on it directly. The second ! does the same for the inner .pop(). The result, el2, is typed simply as string — the compiler has been told to stop worrying about the undefined cases entirely.
Pericolo
! performs no runtime check. If either pop() call actually returns undefined (for example, because one of the arrays was empty), this code throws a runtime error exactly like accessing a property on undefined in plain JavaScript — the compiler simply won’t warn you beforehand anymore.
3.1 ?. vs !: two very different philosophies
?.checks and then moves forward: it verifies the value exists before continuing, and gracefully producesundefinedif it doesn’t.!forces you to move forward: it assumes the value exists, no matter what, and if you’re wrong the program crashes at runtime.
As a general guideline, prefer ?. — it’s inherently safer, since it acknowledges and handles the possibility of a missing value rather than assuming it away. Reach for ! only when you have context the compiler genuinely cannot infer (for example, you just checked the value a few lines above in a way TypeScript’s narrowing can’t follow, or you have an external guarantee that the value will always exist, such as a required DOM element you know is in the page).
4. Nullish coalescing (??)
Optional chaining pairs naturally with the nullish coalescing operator ??, which supplies a default value when the expression on its left is null or undefined.
const el = arr.pop()?.name;
const value = el ?? "default";
console.log(value); // "default" if el was undefined, otherwise the name itself
?? is deliberately narrower than the || operator: || falls back to the right-hand side for any falsy value (0, "", false, NaN, null, undefined), while ?? only falls back for null/undefined. This distinction matters in practice:
const count = 0;
console.log(count || 10); // 10 (0 is falsy, so || replaces it — probably not what you want)
console.log(count ?? 10); // 0 (0 is not null/undefined, so ?? keeps it)
Attenzione
If a legitimate value like 0, "", or false should be preserved, use ?? instead of ||. Using || for defaults is a common source of subtle bugs when zero or empty-string are valid values.
5. Putting it together
A very common real-world pattern combines optional chaining with nullish coalescing to safely read a possibly-missing value and provide a fallback in one expression:
interface User {
profile?: {
nickname?: string;
};
}
function getDisplayName(user: User): string {
return user.profile?.nickname ?? "Anonymous";
}
This single line safely handles: user.profile being undefined, user.profile.nickname being undefined, and falls back to "Anonymous" in either case — with no runtime risk at all, unlike a chain of ! assertions.
6. Further reading
- TypeScript Handbook — Optional Chaining (release notes, TS 3.7)
- MDN — Optional chaining (?.)
- MDN — Nullish coalescing operator (??)
- TypeScript Playground
7. Quiz
Mettiti alla prova
0/7 risposte
What does
arr.pop()?.namedo ifarris empty?What does the
!non-null assertion operator do at runtime?Which operator is generally recommended over the other, and why?
What is the type of
elinconst el = arr.pop()?.name;assuming arr's elements have a stringname?What does
obj.method?.()do ifobj.methoddoes not exist?Given
const count = 0;, what doescount ?? 10evaluate to?Why might
||be a risky choice for providing default values compared to???
8. Exercises
8.1 Exercise
Scenario: You’re building a user profile card component that reads deeply nested, partially optional data coming from an API.
Task:
- Define an interface
Userwith an optionaladdressobject containing an optionalcity: string. - Write a function
getCity(user: User): stringthat uses optional chaining and nullish coalescing to safely return the city or"Unknown city"if missing at any level. - Test the function with a user that has a full address, one with a missing
address, and one with anaddressbut nocity. - Rewrite the same function using
!non-null assertions instead, and explain in a comment why this version is less safe.
Learning objective: compare optional chaining plus nullish coalescing against non-null assertions for handling deeply nested optional data.
8.2 Exercise
Scenario: You maintain a small event system where handlers are optional callback properties on a configuration object.
Task:
- Define an interface
WidgetConfigwith optional callback propertiesonOpen?: () => voidandonClose?: () => void. - Write a function
triggerOpen(config: WidgetConfig): voidthat safely invokesonOpenonly if it exists, using?.(). - Add a counter variable that should default to
0if acountfield on the config isnullorundefined, using??(not||), and explain why||would be the wrong choice ifcountcan legitimately be0.
Learning objective: practice optional chaining on function calls and understand the practical difference between ?? and || for numeric defaults.