TS2531: Object is possibly 'null' fires when TypeScript sees a value that could be null but you access it without checking. This happens with React refs, document.querySelector, and any type that includes null. Fix by guarding, using optional chaining, or narrowing with a type guard. Here is the complete 2026 fix guide.
Why TS2531 happens
With strictNullChecks enabled (default in strict mode), TypeScript tracks null and undefined as distinct types. Any value typed as T | null triggers TS2531 on property or method access. The fix is to narrow the type before use.
Fix 1: React useRef pattern
import { useRef, useEffect } from "react";
// useRef returns { current: HTMLInputElement | null }
const inputRef = useRef<HTMLInputElement>(null);
// WRONG: current can be null
useEffect(() => {
inputRef.current.focus();
// TS2531: Object is possibly 'null'.
}, []);
// FIX 1: optional chaining
inputRef.current?.focus();
// FIX 2: guard with if
if (inputRef.current) {
inputRef.current.focus();
}
// FIX 3: assertion (only if you know it exists at that point)
inputRef.current!.focus();Fix 2: document.querySelector
// WRONG: querySelector returns Element | null
const el = document.querySelector(".modal");
el.classList.add("open");
// TS2531
// FIX 1: guard
if (el) {
el.classList.add("open");
}
// FIX 2: optional chaining
el?.classList.add("open");
// FIX 3: type-safer selector helper
function requireElement<T extends Element>(selector: string): T {
const el = document.querySelector<T>(selector);
if (!el) throw new Error(`Missing element: ${selector}`);
return el;
}
const el = requireElement<HTMLDivElement>(".modal");
el.classList.add("open"); // no TS2531Fix 3: nullable API response
type Profile = { name: string; email: string } | null;
async function loadProfile(): Promise<Profile> {
const res = await fetch("/api/profile");
if (!res.ok) return null;
return await res.json();
}
const profile = await loadProfile();
// profile.name // TS2531
profile?.name; // FIX with ?.
if (profile) {
console.log(profile.name); // narrowed
} else {
console.log("no profile");
}Fix 4: nullish coalescing for defaults
type User = { name: string; nickname: string | null };
const user: User = { name: "Ana", nickname: null };
// WRONG
const display: string = user.nickname;
// TS2322 or downstream TS2531
// FIX with ??
const display: string = user.nickname ?? user.name;
// Use || only when null AND empty string should both fall back
const display: string = user.nickname || user.name;Debugging checklist
- Check
tsconfig.jsonhas"strict": true. TS2531 exists to catch real null bugs. - Hover the value to confirm its type. Look for
| nullor| undefined. - If the value is a React ref, remember that
currentis null until after mount. Guard inuseEffect, not in render. - For DOM lookups, use
querySelectoronly after DOMContentLoaded or in a useEffect hook after mount.
Common mistakes when fixing TS2531
- Using
!non-null assertion everywhere. Fragile. If the value is ever null, you crash at runtime. - Turning off
strictNullChecks. You lose all null safety and reintroduce the exact bug TS2531 catches. - Using
||when??is safer.||treats empty string, 0, and false as null.??only treats null and undefined. - Nested optional chaining without a fallback.
obj?.a?.b?.ccan silently return undefined. Add??for meaningful defaults.
Debugging TypeScript errors systematically
TypeScript errors follow a predictable pattern once you understand the compiler’s logic. Before jumping to fixes, run through this diagnostic checklist.
- Read the FULL error message. The compiler tells you both the actual type it sees and the expected type. Match them side by side to spot the mismatch.
- Check strictNullChecks and strict mode. Many errors trace back to
strict: truein tsconfig.json. If you inherited a codebase with strict mode on, expect more errors than in loose mode. - Look at inferred types with hover. VS Code shows inferred types on hover. Compare what TypeScript sees vs what you assumed.
- Use // @ts-expect-error strategically. When you know better than the compiler temporarily, mark the line. TypeScript flags the annotation when the underlying issue is fixed.
- Isolate the failing line in a minimal reproducer. A standalone
.tsfile with hardcoded inputs often surfaces the root cause faster than debugging in place.
Common TypeScript configuration issues
Many “why does this not compile” issues trace back to tsconfig.json, not your code.
- strict flag toggles many checks at once.
strict: trueenables strictNullChecks, noImplicitAny, strictFunctionTypes, and more. Turning on strict without preparing your code produces hundreds of errors. - Version mismatch between global and project TypeScript. Different TypeScript versions produce different errors. Pin the version in package.json for consistency.
- skipLibCheck can hide real issues. Setting
skipLibCheck: truespeeds up compilation but hides type mismatches in dependencies. Fine for CI but keep it off during development. - Path aliases and moduleResolution mismatches. The
pathsconfig in tsconfig only works with the right moduleResolution setting. Match “bundler” for Vite, “node16” for Node. - Ambient declarations vs modules. Files with imports are modules; files without imports are ambient scripts. Some errors happen because a file is treated as one when you expected the other.
Production TypeScript best practices
Once you get past errors and ship code, keep these habits to prevent recurring problems.
- Enable strict mode from day 1 on new projects. Migrating to strict later is 10x more work.
- Never use
anywithout a good reason.unknownis safer and forces you to narrow before use. - Avoid type assertions (
as) except at API boundaries. Assertions bypass the type system. Use them sparingly at edges (JSON parsing, DOM, external APIs). - Prefer type over interface for new code. Type aliases work everywhere interfaces do, plus support unions, intersections, and mapped types.
- Test type-level logic. Tools like
expect-typeortsdlet you write tests for your type definitions.
Learning resources for TypeScript
Beyond fixing the current error, deeper TypeScript knowledge pays dividends for years. Recommended starting points:
- Official Handbook. typescriptlang.org/docs/handbook. The canonical reference, well-written and up-to-date.
- Total TypeScript by Matt Pocock. Free tutorials and paid courses. Best for going from junior to advanced TypeScript.
- Type Challenges GitHub repo. Interactive problems that build type-level programming skills.
- TypeScript Deep Dive. Free online book, especially good for the “why” behind features.
- Try in the playground. typescriptlang.org/play lets you experiment with types and see errors immediately.
TypeScript version considerations
Compiler errors can change slightly between TypeScript versions. TypeScript 5.0+ introduced better error messages for many common issues. If you are on 4.x, upgrading may automatically improve diagnostic quality without any code change. Check your package.json for the pinned typescript version, then compare against the release notes at github.com/microsoft/TypeScript/releases before upgrading in production.
Official documentation
Frequently Asked Questions
Optional chaining or if guard?
Optional chaining for single-line access. If guard when you need multiple lines of narrowed code. Both are equally safe.
Is ? non-null assertion safe?
Only if you can prove the value cannot be null at that point. Every unproven ! is a future runtime crash. Prefer guards.
Why does useRef return null initially?
React sets ref.current after the component mounts. During the first render, ref.current is null. Use effects to access it safely.
Difference between || and ???
|| falls back on any falsy value including 0 and empty string. ?? only falls back on null and undefined. Use ?? when 0 and empty string are valid inputs.
Should I disable strictNullChecks?
No. You lose the null safety that TypeScript provides. Fix the actual types with guards or optional chaining instead.
