Report #91995
[bug\_fix] Object is possibly 'null' or 'undefined' \(TS2531/TS2533\) inside a callback or closure despite explicit null check outside
TypeScript's control flow analysis does not assume type guards \(narrowing\) persist into closures \(functions/callbacks\) because the variable could be reassigned before the callback executes. To fix, capture the narrowed value in a constant \(\`const\`\) within the guarded block: \`if \(user\) \{ const currentUser = user; setTimeout\(\(\) => console.log\(currentUser.name\), 0\); \}\`. Alternatively, use optional chaining \(\`user?.name\`\) inside the callback, or a non-null assertion \(\`user\!.name\`\) if certain the variable is not reassigned.
Journey Context:
You fetch data resulting in \`let user: User \| null = await fetchUser\(\)\`. You guard against null with \`if \(user\)\`. Inside this block, you call \`setTimeout\(\(\) => \{ console.log\(user.name\); \}, 1000\)\`. TypeScript immediately underlines \`user\` inside the arrow function with "Object is possibly 'null'". You are perplexed because you just checked \`if \(user\)\` two lines above. You hover over \`user\` inside the callback and see the type is still \`User \| null\`. You check if the \`if\` block is properly scoped—it is. You suspect a TypeScript bug and search online. You find the TypeScript FAQ entry "Why doesn't type narrowing work in closures?" which explains that because \`user\` is a \`let\` variable that could be reassigned \(e.g., \`user = null\` before the timeout fires\), the compiler cannot guarantee it's still non-null inside the callback. The solution clicks: you need to capture the current value in a constant. You refactor to \`if \(user\) \{ const currentUser = user; setTimeout\(\(\) => console.log\(currentUser.name\), 1000\); \}\` and the error vanishes because \`currentUser\` is a \`const\` that cannot be reassigned, so TypeScript knows it remains the narrowed type inside the closure. You feel a mix of annoyance at the boilerplate and respect for the soundness of the type system.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T13:00:20.329785+00:00— report_created — created