Nullability: strictNullChecks, Optional Chaining & Nullish Coalescing
10 questions found
What does the 'strictNullChecks' compiler option change about how null and undefined are treated?
Beginner
Without strictNullChecks, null and undefined are silently assignable to EVERY type (a major source of runtime errors). With it enabled, null and undefined become distinct types that must be explicitly included in a type's union (e.g. `string | null`) before a variable can hold them — the compiler then forces you to check for them before use.
// Without strictNullChecks
let name: string = null; // allowed, but risky
// With strictNullChecks: true
// let name: string = null; // Error: 'null' not assignable to 'string'
let name2: string | null = null; // must be explicit
Real-world example
Catching an entire category of 'Cannot read property of undefined' runtime errors at compile time instead of in production.
Common follow-ups: Why is strictNullChecks considered one of the single most valuable strict-mode flags to enable?
tsconfig & Compiler Options
How does the optional chaining operator (?.) work, and what does it return when a link in the chain is null/undefined?
Beginner
?. safely accesses a property, calls a method, or indexes into a value, short-circuiting and returning 'undefined' immediately if the value right before it is null or undefined, instead of throwing a TypeError.
const city = user?.address?.city; // undefined if user or address is null/undefined, no error thrown
user?.greet?.(); // safely calls greet() only if both user and greet exist
Real-world example
Safely accessing a deeply nested, possibly-missing property from an API response without a long chain of manual if-checks.
Common follow-ups: Does optional chaining short-circuit the ENTIRE rest of the expression once it hits a null/undefined, or just that one step?
Types & Interfaces
How does the nullish coalescing operator (??) differ from the logical OR operator (||) for providing default values?
Beginner
?? only falls back to its right-hand side when the left-hand side is SPECIFICALLY null or undefined; || falls back for ANY falsy value, including 0, '', NaN, and false — which can cause bugs when a legitimately valid falsy value gets incorrectly overridden by a default.
const count = 0;
console.log(count || 10); // 10 -- WRONG, treats valid 0 as 'missing'
console.log(count ?? 10); // 0 -- correct, 0 is a valid value, not null/undefined
Real-world example
Providing a default page size or quantity value where 0 is a legitimately valid input that shouldn't be overridden.
Common follow-ups: Why can't you mix ?? and || directly in the same expression without parentheses?
Types & Interfaces
How does the non-null assertion operator (!) differ from actually checking and narrowing a value?
Intermediate
The ! operator tells the COMPILER 'trust me, this is definitely not null/undefined here', silencing the type error WITHOUT adding any runtime check — if you're wrong, it still throws a runtime TypeError just like plain JavaScript would, whereas a real if-check or optional chaining actually protects against that at runtime.
function getLength(value: string | null) {
return value!.length; // compiles, but throws at runtime if value is actually null
}
// Safer alternative:
function getLengthSafe(value: string | null) {
return value?.length ?? 0;
}
Real-world example
Using ! sparingly and only when you have external knowledge the compiler can't infer, like a DOM query you know will always succeed.
Common follow-ups: Why is overusing the non-null assertion operator considered a common TypeScript anti-pattern?
Type Narrowing & Guards
How do you combine optional chaining with the nullish coalescing operator to safely access a value with a fallback default?
Intermediate
Chain ?. through the potentially-missing path, then use ?? immediately after to provide a fallback specifically for the case where the whole chain resolved to undefined (either because it stopped early or the final value itself was null/undefined).
const theme = user?.settings?.theme ?? 'light';
// Falls back to 'light' if user, settings, or theme itself is null/undefined
Real-world example
Providing a sensible default UI theme or configuration value when a user's saved preferences might not exist yet.
Common follow-ups: What's the difference in behavior if you used || instead of ?? at the end of this chain?
Union & Intersection Types
How does the optional chaining call syntax (?.()) differ from just optional property access, and when is it needed?
Intermediate
?.() specifically guards against the FUNCTION ITSELF being null/undefined before attempting to call it — necessary when a property might hold an optional callback function; plain ?. alone only guards property access, not the act of invoking what it resolves to.
interface Options {
onComplete?: () => void;
}
function run(options: Options) {
options.onComplete?.(); // safely calls onComplete only if it was actually provided
}
Real-world example
Safely invoking an optional callback prop or event handler that a caller may or may not have supplied.
Common follow-ups: Does optional chaining also work with array/bracket-style indexing, like arr?.[0]?
Function Types
Overloads & Optional/Default Parameters
How does control-flow-based narrowing interact with strictNullChecks across an async function's awaited boundaries?
Advanced
TypeScript's narrowing analysis generally does NOT persist across an 'await' boundary if the awaited expression or an intervening call could theoretically have side effects that invalidate the earlier narrowing (like a captured variable being reassigned) — a variable narrowed as non-null before an await may need re-checking afterward, since the compiler conservatively assumes it MIGHT have changed.
async function process(user: User | null) {
if (user !== null) {
await someAsyncOperation(); // narrowing may not survive if 'user' isn't a local const
console.log(user.name); // TypeScript may still trust it here for a simple local variable, but NOT for a class property
}
}
Real-world example
Debugging a surprising re-widening of a null-checked value's type after an await, particularly with object properties rather than local variables.
Common follow-ups: Why does this narrowing loss happen specifically for object PROPERTIES but often NOT for simple local 'const' variables?
Type Narrowing & Guards
How would you write a generic 'NonNullable' utility to strip null and undefined from a type, and how does the built-in version work?
Advanced
The built-in NonNullable<T> is implemented as a conditional type: `type NonNullable<T> = T extends null | undefined ? never : T;` — since it's a distributive conditional type, applying it to a union automatically removes just the null/undefined members while preserving everything else.
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>; // string
type B = NonNullable<number | null>; // number
Real-world example
Deriving a 'safe' version of a type after establishing (via runtime checks) that a value can no longer be null/undefined in a given code path.
Common follow-ups: How does this rely on the distributive behavior of conditional types over a union, specifically?
Conditional Types
How do you correctly type an object property that's genuinely optional in one sense but should never be explicitly set to 'undefined', using 'exactOptionalPropertyTypes'?
Advanced
By default, `prop?: string` allows BOTH omitting the property entirely AND explicitly assigning it the value 'undefined' — these are usually treated as equivalent. Enabling exactOptionalPropertyTypes distinguishes them: explicitly setting a property to undefined becomes a type error unless 'undefined' is also included in its declared type, catching a subtle but meaningful difference some APIs (like some ORMs) actually care about.
// tsconfig.json: { "exactOptionalPropertyTypes": true }
interface Config {
timeout?: number;
}
const c: Config = { timeout: undefined }; // Error with exactOptionalPropertyTypes: true
const c2: Config = {}; // OK -- omitting entirely is fine
Real-world example
Catching a subtle bug in an ORM or API client where explicitly setting a field to undefined has different runtime semantics than omitting it entirely.
Common follow-ups: Why might some libraries genuinely distinguish between 'omitted' and 'explicitly undefined' at runtime?
tsconfig & Compiler Options
How do user-defined type predicates interact with strictNullChecks when narrowing away null specifically (as opposed to a broader type check)?
Advanced
A type predicate function like `(value): value is T` can specifically be written to narrow away JUST null (or undefined) from a broader union, which is especially useful as a reusable, named alternative to writing the same `!== null` check inline repeatedly across a codebase, particularly inside array methods like .filter() where inline narrowing doesn't otherwise propagate to the resulting array's type.
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
const maybeUsers: (User | null)[] = [user1, null, user2];
const users: User[] = maybeUsers.filter(isNotNull); // correctly typed as User[], not (User | null)[]
Real-world example
Filtering out null entries from an array while getting the compiler to correctly recognize the result no longer contains null, unlike a plain inline filter callback.
Common follow-ups: Why does `maybeUsers.filter(u => u !== null)` NOT automatically produce a User[] result the way the isNotNull predicate does?
Type Narrowing & Guards