Fix: TS2532 Object is possibly 'undefined' / Object is possibly 'null'
The Error
You write TypeScript code that compiles fine in your head, but the compiler disagrees:
error TS2532: Object is possibly 'undefined'.error TS2532: Object is possibly 'null'.Or in TypeScript 5.0+, the more specific variant:
error TS18048: 'user' is possibly 'undefined'.error TS18047: 'user' is possibly 'null'.All of these mean the same thing: you’re accessing a property or calling a method on a value that TypeScript thinks could be undefined or null. TypeScript won’t let you do that because it would crash at runtime.
Why This Happens
TypeScript’s type system tracks whether a value can be null or undefined. When strictNullChecks is enabled (it’s on by default in most modern configs, and it’s part of strict: true), TypeScript treats null and undefined as distinct types that aren’t assignable to other types.
Here’s the simplest reproduction:
function getUser(id: string): User | undefined {
return users.find(u => u.id === id);
}
const user = getUser('123');
console.log(user.name);
// ~~~~
// error TS2532: Object is possibly 'undefined'.TypeScript sees that getUser can return undefined. You’re accessing .name without first proving that user isn’t undefined. That’s a potential runtime crash, so TypeScript stops you.
This happens in many common scenarios:
Array.find()returnsT | undefinedMap.get()returnsV | undefined- Optional properties (
name?: string) arestring | undefined document.getElementById()returnsHTMLElement | null- React refs (
useRef<T>(null)) start asnull - Object index signatures (
Record<string, T>) returnT | undefined
TypeScript is protecting you. Every one of these can legitimately be undefined or null at runtime. If you ignore these warnings, you’ll end up with TypeError: Cannot read properties of undefined at runtime. The fix is to handle that possibility explicitly.
Fix
1. Type Narrowing with if Checks
The most straightforward fix. Check for null or undefined before using the value. TypeScript is smart enough to narrow the type inside the if block.
const user = getUser('123');
if (user) {
console.log(user.name); // TypeScript knows user is User here
}This works because TypeScript performs control flow analysis. After the if (user) check, it knows user can’t be undefined or null inside the block.
Different narrowing patterns:
// Truthiness check (excludes null, undefined, 0, '', false)
if (user) { ... }
// Strict equality (most precise)
if (user !== undefined) { ... }
if (user !== null) { ... }
if (user != null) { ... } // excludes both null AND undefined
// typeof check (useful for union types)
if (typeof value === 'string') { ... }Use != null (loose equality) when you want to exclude both null and undefined in one check.
2. Optional Chaining (?.)
When you want to access a nested property but any part of the chain might be undefined or null:
const user = getUser('123');
console.log(user?.name); // string | undefined (no error)
console.log(user?.address?.city); // safe nested accessOptional chaining short-circuits. If user is undefined, the entire expression returns undefined instead of throwing.
It works with method calls and bracket notation too:
user?.getProfile?.() // safe method call
user?.['first-name'] // safe bracket access
users?.[0]?.name // safe array index access3. Nullish Coalescing (??)
Combine with optional chaining to provide a fallback value:
const name = user?.name ?? 'Anonymous';
const city = user?.address?.city ?? 'Unknown';Use ?? instead of ||. The || operator treats '', 0, and false as falsy and replaces them with the fallback. The ?? operator only triggers on null and undefined:
const count = user?.postCount ?? 0; // correct: 0 stays as 0
const count = user?.postCount || 0; // bug: if postCount is 0, it becomes 0 anyway... but '' || 'default' would be 'default'4. Non-Null Assertion Operator (!)
If you know a value isn’t null or undefined but TypeScript can’t prove it, use the ! operator:
const user = getUser('123');
console.log(user!.name); // you promise TypeScript this is not null/undefinedUse this sparingly. The ! operator tells TypeScript to shut up. If you’re wrong, you get a runtime crash — the exact thing TypeScript was trying to prevent. It has valid use cases (see below), but reach for type narrowing or optional chaining first.
Valid use case — when you’ve already checked but TypeScript lost track:
const users = new Map<string, User>();
if (users.has(id)) {
// TypeScript doesn't narrow Map.get() after .has()
const user = users.get(id)!; // safe: you just checked .has()
}5. Array.find() Returning undefined
Array.find() returns T | undefined because the element might not exist. This is one of the most common triggers.
const users: User[] = [{ id: '1', name: 'Alice' }];
const user = users.find(u => u.id === '1');
console.log(user.name);
// ~~~~ Object is possibly 'undefined'.Fix — narrow with an if check:
const user = users.find(u => u.id === '1');
if (!user) {
throw new Error('User not found');
}
console.log(user.name); // TypeScript knows user is User after the throwFix — when you genuinely know it exists:
// Use non-null assertion only when you're certain
const user = users.find(u => u.id === '1')!;Fix — use a type-safe helper that throws:
function findOrThrow<T>(arr: T[], predicate: (item: T) => boolean): T {
const result = arr.find(predicate);
if (!result) throw new Error('Element not found');
return result;
}
const user = findOrThrow(users, u => u.id === '1'); // type: User6. Map.get() Returning undefined
Map.get() returns V | undefined because the key might not exist. TypeScript doesn’t narrow this even after a .has() check.
const cache = new Map<string, User>();
const user = cache.get('alice');
console.log(user.name);
// ~~~~ Object is possibly 'undefined'.Fix — use the value from the narrowing check:
const user = cache.get('alice');
if (user) {
console.log(user.name); // narrowed to User
}Fix — use non-null assertion after .has():
if (cache.has('alice')) {
const user = cache.get('alice')!; // safe after .has()
console.log(user.name);
}7. Optional Properties
Properties marked with ? are T | undefined:
interface Config {
database?: {
host: string;
port: number;
};
}
function connect(config: Config) {
console.log(config.database.host);
// ~~~~~~~~~~~~~~~ Object is possibly 'undefined'.
}Fix — narrow or use optional chaining:
function connect(config: Config) {
if (!config.database) {
throw new Error('Database config is required');
}
console.log(config.database.host); // narrowed
}
// Or with optional chaining + fallback:
const host = config.database?.host ?? 'localhost';8. React Refs (useRef)
React refs initialized with null have type T | null. You hit this error when you access .current properties:
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current.focus();
// ~~~~~~~ Object is possibly 'null'.
}Fix — null check before access:
function focusInput() {
if (inputRef.current) {
inputRef.current.focus();
}
// Or:
inputRef.current?.focus();
}If you’re using refs inside conditional hooks, make sure you’re not violating the Rules of Hooks. For refs that are always set after mount (attached to a DOM element that’s always rendered), some codebases use the non-null assertion:
const inputRef = useRef<HTMLInputElement>(null!);This tells TypeScript the ref will never actually be null when you access it. Only do this if the ref is guaranteed to be attached — if the element conditionally renders, you’ll crash.
9. Definite Assignment Assertion
When you declare a variable and assign it later (in a way TypeScript can’t track), use ! in the declaration:
let connection: DatabaseConnection;
// Assigned in init() which is always called before use
async function init() {
connection = await createConnection();
}
function query(sql: string) {
return connection.execute(sql);
// ~~~~~~~~~~ Variable 'connection' is used before being assigned.
}Fix — definite assignment assertion:
let connection!: DatabaseConnection; // the ! tells TypeScript: trust me, it'll be assignedUse this in class properties too:
class App {
private db!: Database; // assigned in init(), not in the constructor
async init() {
this.db = await connectToDatabase();
}
}10. Custom Type Guards
When you have complex logic to determine whether a value is defined, write a type guard:
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const users: (User | undefined)[] = [getUser('1'), getUser('2')];
// Filter out undefined values with proper typing
const definedUsers: User[] = users.filter(isDefined);Without the type guard, users.filter(u => u !== undefined) still types the result as (User | undefined)[]. The type guard tells TypeScript what the filter actually does.
This is also powerful for discriminated unions:
interface SuccessResponse {
status: 'success';
data: User;
}
interface ErrorResponse {
status: 'error';
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(res: ApiResponse): res is SuccessResponse {
return res.status === 'success';
}
function handleResponse(res: ApiResponse) {
if (isSuccess(res)) {
console.log(res.data.name); // TypeScript knows res is SuccessResponse
}
}11. Exhaustive Checks with never
When you use switch statements or conditional chains on discriminated unions, TypeScript narrows types at each branch. Use never to ensure you handle every case:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
default:
const _exhaustive: never = shape; // compile error if a case is missing
return _exhaustive;
}
}If someone adds a 'triangle' variant to Shape and forgets to add a case here, TypeScript will catch it at compile time.
strictNullChecks in tsconfig.json
If you’re not seeing these errors at all, strictNullChecks might be off. Check your tsconfig.json:
{
"compilerOptions": {
"strict": true
}
}strict: true enables strictNullChecks along with other strict flags. You can also enable it individually:
{
"compilerOptions": {
"strictNullChecks": true
}
}Keep it on. Turning off strictNullChecks silences these errors but doesn’t fix the underlying bugs. Your code can still crash at runtime — TypeScript just stops warning you about it.
If you’re enabling it on a large existing codebase and get hundreds of errors, adopt it incrementally. Fix one file at a time. Use // @ts-expect-error to temporarily suppress errors you’ll fix later, rather than turning the whole flag off.
Still Not Working?
TypeScript Doesn’t Narrow After Array.includes() or Set.has()
TypeScript doesn’t narrow types after .includes() or .has() checks the way you might expect:
const validStatuses = ['active', 'pending'] as const;
type Status = typeof validStatuses[number];
function process(status: string) {
if (validStatuses.includes(status as Status)) {
// status is still `string` here, not narrowed
}
}Use a type guard instead:
function isValidStatus(status: string): status is Status {
return (validStatuses as readonly string[]).includes(status);
}Narrowing Doesn’t Persist Across Callbacks
TypeScript resets narrowing inside callbacks because the callback might execute later when the variable could have changed:
const user = getUser('123');
if (user) {
setTimeout(() => {
console.log(user.name); // this actually works (closure captures the narrowed value)
}, 1000);
someArray.forEach(() => {
// works here too, because `user` is const and captured by closure
});
}But this fails when the variable can be reassigned:
let user = getUser('123');
if (user) {
setTimeout(() => {
console.log(user.name);
// ~~~~ Object is possibly 'undefined'.
}, 1000);
}Fix: use const instead of let, or capture the narrowed value in a const:
let user = getUser('123');
if (user) {
const currentUser = user; // capture narrowed value
setTimeout(() => {
console.log(currentUser.name); // works
}, 1000);
}document.getElementById Always Returns null Type
DOM methods like getElementById, querySelector, and closest return nullable types. Even if you’re sure the element exists:
const el = document.getElementById('app');
el.classList.add('loaded');
// ~~ Object is possibly 'null'.Fix with narrowing and a helpful error message:
const el = document.getElementById('app');
if (!el) {
throw new Error('Could not find #app element. Check your HTML.');
}
el.classList.add('loaded'); // narrowed to HTMLElementOr if you need a specific element type:
const input = document.getElementById('email') as HTMLInputElement | null;
if (input) {
input.value = 'test@example.com'; // narrowed to HTMLInputElement
}Conflicting Type Assertions and the object is possibly undefined Error in Generics
When you use generics with constraints, TypeScript sometimes can’t tell that a property access is safe:
function getFirst<T extends { items?: string[] }>(obj: T) {
return obj.items[0];
// ~~~~~ Object is possibly 'undefined'.
}The optional items property means it could be undefined. Narrow it:
function getFirst<T extends { items?: string[] }>(obj: T) {
if (!obj.items || obj.items.length === 0) {
return undefined;
}
return obj.items[0];
}Consider Using the satisfies Operator
TypeScript 4.9+ introduced satisfies, which can help preserve narrower types while still validating against a wider type:
type Route = {
path: string;
handler?: () => void;
};
// With `satisfies`, TypeScript knows exactly which routes have handlers
const routes = {
home: { path: '/', handler: () => {} },
about: { path: '/about' },
} satisfies Record<string, Route>;
routes.home.handler(); // no error — TypeScript knows handler exists on home
routes.about.handler();
// ~~~~~~~ Property 'handler' does not existRelated: Fix: TypeError: Cannot read properties of undefined | Fix: TS2307 Cannot find module or its corresponding type declarations | Fix: ESLint Parsing error: Unexpected token
Related Articles
Fix: TS2322 Type 'X' is not assignable to type 'Y'
How to fix TypeScript error TS2322 'Type is not assignable to type'. Covers literal types vs general types, string vs String, union types, interface compatibility, generic constraints, readonly arrays, excess property checking, discriminated unions, type assertions, type widening and narrowing, React event handlers, Promise return types, and enum mismatches.
Fix: Module not found: Can't resolve / Cannot find module or its corresponding type declarations
How to fix 'Module not found: Can't resolve' in webpack, Vite, and React, and 'Cannot find module or its corresponding type declarations' in TypeScript. Covers missing packages, wrong import paths, case sensitivity, path aliases, node_modules corruption, monorepo hoisting, barrel files, and asset imports.
Fix: [vite] Internal server error: Failed to resolve import
How to fix Vite's 'Failed to resolve import' error, including 'Does the file exist?', 'Optimized dependency needs to be force included', 'Pre-transform error', and '504 (Outdated Optimize Dep)'. Covers missing packages, path aliases, optimizeDeps, cache clearing, and CJS/monorepo edge cases.
Fix: TypeError: Cannot read properties of undefined (reading 'xxx')
How to fix 'TypeError: Cannot read properties of undefined', 'Cannot read property of undefined', and 'Cannot read properties of null' in JavaScript, TypeScript, and React. Covers optional chaining, async data fetching, destructuring, and nested object access.