Learn why TypeScript throws TS2531 when accessing properties on potentially null values, and how to handle null safely with type narrowing and optional chaining.
TS2531 tells you that you're trying to use a value that could be null. Under strictNullChecks, TypeScript won't let you access properties or call methods on a value unless it's sure the value isn't null.
This directly prevents one of JavaScript's most infamous runtime errors: TypeError: Cannot read properties of null. If you've ever called document.getElementById and forgotten to check the result, you've hit exactly the bug this error protects against.
// The general shape of the error:
// Object is possibly 'null'.
//
// TypeScript sees: someValue: SomeType | null
// You wrote: someValue.property — not safe!nullDOM methods like getElementById, querySelector, and closest all return null when no matching element is found.
// ❌ Broken
const loginForm = document.getElementById("login-form")
loginForm.addEventListener("submit", handleSubmit)
// ~~~~~~~ Error: Object is possibly 'null'.// ✅ Fixed — check for null before using the element
const loginForm = document.getElementById("login-form")
if (loginForm) {
loginForm.addEventListener("submit", handleSubmit)
}
// Or with optional chaining (for one-off access)
document.getElementById("login-form")?.addEventListener("submit", handleSubmit)Many APIs return null for missing or empty fields. If your types reflect this, you need to handle the null case.
// ❌ Broken
interface GitHubUser {
login: string
name: string | null
bio: string | null
company: string | null
}
function displayUserBio(user: GitHubUser) {
const bioLength = user.bio.length
// ~~~~~~~~ Error: Object is possibly 'null'.
}// ✅ Fixed — provide a fallback for null values
function displayUserBio(user: GitHubUser) {
const bioLength = user.bio?.length ?? 0
const displayBio = user.bio ?? "This user hasn't written a bio yet."
}Array.prototype.match() and Regex ResultsString.prototype.match() returns null when the pattern doesn't match.
// ❌ Broken
function extractDomain(email: string): string {
const match = email.match(/@(.+)$/)
return match[1]
// ~~~~~ Error: Object is possibly 'null'.
}// ✅ Fixed — handle the no-match case
function extractDomain(email: string): string {
const match = email.match(/@(.+)$/)
if (!match) {
throw new Error(`Invalid email format: ${email}`)
}
return match[1]
}TypeScript's narrowing doesn't persist across await or callback boundaries, because the value could change in between.
// ❌ Broken
let activeConnection: WebSocket | null = null
async function sendMessage(message: string) {
if (activeConnection) {
await delay(100)
activeConnection.send(message)
// ~~~~~~~~~~~~~~~~ Error: Object is possibly 'null'.
// TypeScript can't guarantee it's still non-null after await
}
}// ✅ Fixed — capture in a local constant before await
async function sendMessage(message: string) {
const connection = activeConnection
if (connection) {
await delay(100)
connection.send(message) // OK — local const can't be reassigned
}
}ifThe most explicit and readable approach. TypeScript narrows the type inside the block.
const element = document.querySelector(".modal")
if (element !== null) {
element.classList.add("visible") // OK — narrowed to Element
}?.)Short-circuits the expression if the value is null (or undefined). Returns undefined instead of throwing.
const headerText = document.querySelector("h1")?.textContent
// headerText: string | null | undefined??)Provides a default when the value is null or undefined.
const username = response.data.user?.name ?? "Anonymous"Useful in functions where a null value means you should stop processing entirely.
function initializeChart(containerId: string) {
const container = document.getElementById(containerId)
if (!container) {
console.warn(`Chart container #${containerId} not found`)
return
}
// From here on, container is guaranteed to be HTMLElement
container.style.width = "100%"
renderChart(container)
}!) — Use with Caution// Only when you have certainty TypeScript can't verify
const app = document.getElementById("app")! // you control the HTMLTS2531 occurs when you access a property or method on a value that could be null. Common sources include:
getElementById, querySelector, closest, parentElement all return null when no element matches.null for missing or empty fields (GitHub's API is a notable example).String.match() returns null when the pattern doesn't match.JSON.parse can produce null for the input "null".T | null requires narrowing.The best approach depends on context:
For DOM elements, use the early return pattern or an if check. DOM queries commonly return null and you should always handle that case.
For API data, use optional chaining with nullish coalescing: user.bio ?? "No bio".
For regex matches, check for null and either throw an error or handle the no-match case explicitly.
Avoid blanket use of ! (non-null assertion) as it provides no runtime safety. If the value turns out to be null, you'll get the exact same TypeError that TypeScript was trying to prevent.
Both errors protect you from the same class of runtime error (TypeError: Cannot read properties of null/undefined), but they arise from different sources:
TS2531 (null) | TS2532 (undefined) | |
|---|---|---|
| Source | DOM queries, API responses, explicit null returns | Optional properties, missing Map entries, Array.find() |
| Meaning | "This value was explicitly set to nothing" | "This value was never set" |
| Fix | Same toolset: ?., ??, if checks, ! | Same toolset: ?., ??, if checks, ! |
In practice, you'll often see both together. A value typed as string | null | undefined can trigger either error depending on which branch TypeScript is analyzing. Optional chaining (?.) and nullish coalescing (??) handle both null and undefined, making them the most convenient fix for either error.
Get the latest TypeScript tips, tutorials, and course updates delivered straight to your inbox.