Problem Solved

TypeScript · possibly null

Home › Courses › TypeScript › Problems › Object is possibly null
🔷 Problem · 5 min read

"Object is possibly null".
Four ways to fix it safely.

You enabled strictNullChecks, and now half your codebase has red squiggles: "Object is possibly 'null'" or "possibly 'undefined'". This is the compiler doing its job — those null checks you skipped for years are now surfaced. Here are the four ways to handle it, from safest to riskiest.

Short answer: Use the right fix for the case. Early return (if (!obj) return) for functions that shouldn't run without the value. Optional chaining (obj?.prop) for potentially missing properties. Nullish coalescing (obj ?? fallback) when you have a sensible default. Non-null assertion (obj!.prop) only when you've verified the value elsewhere — and even then, rarely. Every fix is compile-time only; the runtime behavior depends on which you pick.

✅ Solution 1 — Early return (safest)

Narrow once, use safely for the rest of the function.

The cleanest fix: check at the top of the function, return early if the value is null, and use the narrowed type for the rest. TypeScript's control flow analysis tracks this — after the early return, the value is non-null for the rest of the scope. No !, no ?., no noise.

early-return.ts
interface User {
id: string;
name: string;
email: string;
}

// ─── The error ───────────────────────────────
function greet(user: User | null): string {
// return Hello, ${user.name}`;
// ❌ 'user' is possibly 'null'
return '';
}

// ─── Fix 1: early return ─────────────────────
function greet2(user: User | null): string {
if (!user) {
return 'Hello, guest';
}
// user is User here — the null case returned
return Hello,${user.name}`; // ✅ no error
}

// ─── Early return with throw ─────────────────
function mustGreet(user: User | null): string {
if (!user) {
throw new Error('User required');
}
return Hello, ${user.name}`; // ✅ narrowed after throw
}

// ─── Guard clauses — multiple checks ─────────
function display(user: User | null, prefix?: string): string {
if (!user) return '(no user)';
if (!prefix) return user.name;
return ``${prefix} ${user.name};
}

// ─── Why it's the safest fix ─────────────────
//
// 1. No non-null assertions — the compiler knows.
// 2. No optional chaining everywhere.
// 3. No silent undefined → the caller sees the
// fallback value or a thrown error explicitly.
// 4. Idiomatic — every TypeScript style guide
// recommends this pattern.

const alice: User = { id: '1', name: 'Alice', email: 'a@x.com' };

console.log(greet2(alice), greet2(null));
console.log(display(alice, 'Hi'), display(null));
try { mustGreet(null); } catch (e) { console.log(e); }
function greet(user) {
return '';
}

function greet2(user) {
if (!user) {
return 'Hello, guest';
}
return Hello,${user.name}`;
}

function mustGreet(user) {
if (!user) {
throw new Error('User required');
}
return Hello, ${user.name}`;
}

function display(user, prefix) {
if (!user) return '(no user)';
if (!prefix) return user.name;
return ``${prefix} ${user.name}`;
}

const alice = { id: '1', name: 'Alice', email: 'a@x.com' };

console.log(greet2(alice), greet2(null));
console.log(display(alice, 'Hi'), display(null));
try { mustGreet(null); } catch (e) { console.log(e); }

Narrow once

One if (!user) return narrows the type for the rest of the function. No repeated checks, no ?..

Explicit fallback

Return a real value or throw. The caller knows exactly what happens when the value is null — no silent undefined behavior.

Works after throw

if (!user) throw ... also narrows — after the throw, user is User. Ideal for functions that require a value.

Guard clauses stack

Multiple early returns flatten nesting. Each check handles one condition and exits. The happy path is at the bottom, unindented.

Adopt guard clauses as a habit before you enable strict. The biggest migration pain with strictNullChecks is that every function that used to work now has red squiggles. The teams that migrate smoothly do so because they already write in the guard-clause style — check at the top, use at the bottom. It's not just an error fix; it's better code. Flatter, more readable, and the compiler helps you. Retrofitting this style during a strict migration is much harder than adopting it now.

✅ Solution 2 — Optional chaining (?.)

When the property might genuinely be missing.

For property access on a value that might be null or undefined, obj?.prop is the cleanest syntax. It short-circuits to undefined if obj is null, avoiding the crash. This is the right fix when "property is missing" is a legitimate state — not an error condition.

optional-chaining.ts
interface User {
id: string;
name: string;
address?: { city: string; zip: string };
}

// ─── Optional chaining on nullable objects ───
function getCity(user: User | null): string | undefined {
return user?.address?.city;
// ↑ ↑
// user might be null
// address might be undefined
// Returns undefined if either is missing
}

// ─── Optional chaining on methods ────────────
function getUpperName(user: User | null): string | undefined {
return user?.name.toUpperCase(); // if user null → undefined
}

// ─── Optional chaining on arrays ─────────────
function firstTag(tags?: string[]): string | undefined {
return tags?.[0]; // if tags undefined → undefined
}

// ─── Optional chaining on function calls ─────
function callHandler(handler?: () => void): void {
handler?.(); // if handler undefined → does nothing
}

// ─── Combined with nullish coalescing ────────
function getCityOr(user: User | null, fallback = 'Unknown'): string {
return user?.address?.city ?? fallback;
// ↑ optional chain ↑ nullish fallback
// If city is undefined → fallback
}

// ─── What it compiles to ─────────────────────
// user?.address?.city
// →
// user === null || user === undefined ? undefined :
// user.address === null || user.address === undefined ? undefined :
// user.address.city
//
// Much cleaner to write, same semantics.

const alice: User = {
id: '1', name: 'Alice',
address: { city: 'Springfield', zip: '12345' }
};
const bob: User = { id: '2', name: 'Bob' };

console.log(getCity(alice), getCity(bob), getCity(null));
console.log(getCityOr(bob, 'N/A'));
console.log(getUpperName(alice));
console.log(firstTag(['a', 'b']));
function getCity(user) {
return user?.address?.city;
}

function getUpperName(user) {
return user?.name.toUpperCase();
}

function firstTag(tags) {
return tags?.[0];
}

function callHandler(handler) {
handler?.();
}

function getCityOr(user, fallback = 'Unknown') {
return user?.address?.city ?? fallback;
}

const alice = {
id: '1', name: 'Alice',
address: { city: 'Springfield', zip: '12345' }
};
const bob = { id: '2', name: 'Bob' };

console.log(getCity(alice), getCity(bob), getCity(null));
console.log(getCityOr(bob, 'N/A'));
console.log(getUpperName(alice));
console.log(firstTag(['a', 'b']));

?. short-circuits

obj?.prop returns undefined if obj is null or undefined. Same for ?.[ for arrays and ?.( for functions.

Chains work

a?.b?.c?.d — every link is checked. If any is null, the whole expression is undefined. Deep property access made safe.

Return type is T | undefined

The result of obj?.prop is string | undefined, not string. The undefined reflects the possibility that obj was null.

Combine with ??

obj?.prop ?? fallback — the safest idiom. If obj is null or prop is undefined, you get the fallback. No exceptions, no crashes.

✅ Solution 3 — Nullish coalescing (??) and non-null assertion

The default value pattern, plus the risky shortcut.

?? provides a default when the left side is null or undefined. It's the cleanest way to say "use this, or fall back to that." The non-null assertion (!) is the opposite — it tells the compiler "trust me" and disables the check entirely. Both are useful in specific cases, but ?? is safe and ! is not.

coalescing.ts
// ─── Nullish coalescing — fallback values ────
interface Config {
port?: number;
host?: string;
debug?: boolean;
}

// ─── ?? provides a fallback for null/undefined
function getPort(cfg: Config): number {
return cfg.port ?? 3000;
}

// The difference between ?? and ||:
// cfg.port ?? 3000 → only null/undefined trigger the fallback
// cfg.port || 3000 → ALSO triggers on 0, '', false
//
// For config values where 0 is valid, use ??.

const cfg1: Config = { port: 0 };
console.log(cfg1.port ?? 3000); // 0 (correct!)
console.log(cfg1.port || 3000); // 3000 (probably wrong)

// ─── Combined ?? and ?. — the safe idiom ─────
function getHost(cfg: Config | null): string {
return cfg?.host ?? 'localhost';
}

// ─── Non-null assertion — trust me ───────────
// The ! operator removes null and undefined
// from the type, without any runtime check.

function processUser(user: User | null): string {
// We checked above and know it's non-null:
if (!user) throw new Error('user required');

// Now user is User — no ! needed. TypeScript
// narrows after the throw, so the following is
// just for demonstration:

return user.name; // ✅ no ! needed
}

// When ! is legitimately needed:
//
// 1. External library that returns T | null but
// you know it's T after some invariant.
// 2. After a custom check the compiler can't see:
// if (user !== null && 'name' in user) { ... }
// TypeScript narrows 'name' but not 'user'.
// 3. Immediately after an assignment in a
// synchronous block where you control the value.

function legitUse(user: User | null): string {
if (user === null) throw new Error('required');
return user.name; // compiler narrows — no ! needed
}

// ─── When NOT to use ! ───────────────────────
//
// ❌ function f(user: User | null) {
// return user!.name;
// }
// If user is null → runtime TypeError.
//
// ❌ function f(users: User[]) {
// return users[0]!.name;
// }
// If array is empty → runtime TypeError.
//
// Every ! is a promise the compiler can't verify.
// Prefer ?? or early return.

console.log(getPort({}), getPort({ port: 8080 }));
console.log(getHost(null), getHost({ host: 'example.com' }));
console.log(processUser({ id: '1', name: 'Alice', email: 'a@x.com' }));
function getPort(cfg) {
return cfg.port ?? 3000;
}

const cfg1 = { port: 0 };
console.log(cfg1.port ?? 3000);
console.log(cfg1.port || 3000);

function getHost(cfg) {
return cfg?.host ?? 'localhost';
}

function processUser(user) {
if (!user) throw new Error('user required');
return user.name;
}

function legitUse(user) {
if (user === null) throw new Error('required');
return user.name;
}

console.log(getPort({}), getPort({ port: 8080 }));
console.log(getHost(null), getHost({ host: 'example.com' }));
console.log(processUser({ id: '1', name: 'Alice', email: 'a@x.com' }));

?? only for null/undefined

x ?? fallback — fallback only if x is null or undefined. Unlike ||, which also triggers on 0, '', false.

?. + ?? combo

cfg?.host ?? 'localhost' — safe access + default value. The idiomatic safe read for config, props, optional data.

! bypasses the check

user!.name tells the compiler "trust me." No runtime check. If wrong, you crash. Use almost never.

When ! is legit

After a check the compiler can't track (custom guard), or with a library that mistypes its return. Even then, prefer alternatives.

Every ! in your codebase is a potential production bug. When you write user!.name, you're saying "I checked this somewhere and the compiler can't tell." But if that "somewhere" is wrong, or the invariant breaks, or the code changes and the check is removed — you crash at runtime, in production, on a Friday afternoon. ! is a promise the compiler can't verify. Use it only after a guard the compiler can't express (if ('name' in user)), and only with a comment explaining why. Every ! is a bug waiting to happen.

⚠️ Common mistakes

The null/undefined handling bugs that cost real time.

1. Using ! instead of proper handling

user!.name — bypasses the check. If user is null, crash. Fix: early return, optional chaining, or nullish coalescing.

2. Confusing ?? and ||

x || fallback triggers on 0, '', false. Fix: use ?? unless you truly want all falsy values to trigger the fallback.

3. Optional chaining everywhere

Every ?. on a required property hides real bugs. Fix: use ?. only for genuinely optional data (config fields, optional params).

4. Narrowing lost after async

if (user) { await something(); user.name } — user might be reassigned. Fix: capture the narrowed value in a const before the await.

5. Narrowing lost after callbacks

if (user) { setTimeout(() => user.name, 0) } — narrowing doesn't cross function boundaries. Fix: capture in a const.

6. Checking the wrong variable

if (user.address) { user.address.city } — narrows user.address, not user. Fix: check the full chain or use optional chaining.

7. Forgetting array access can be undefined

users[0].name — users[0] is User | undefined. Fix: check, or use users[0]?.name, or users.at(0)?.name.

8. Disabling strictNullChecks

Turning it off to make the errors go away. Fix: keep it on. The errors are real bugs — every one is a potential runtime crash.

❓ Frequently asked

How do I fix "Object is possibly null" in TypeScript?

Four approaches depending on the case. (1) Early return: if (!obj) return; — narrows obj for the rest of the function. (2) Optional chaining: obj?.prop. (3) Nullish coalescing: obj ?? fallback. (4) Non-null assertion: obj!.prop — tells the compiler to trust you, but doesn't check at runtime. Prefer the first three.

Why does TypeScript say "possibly null" if my code checks it?

Because the check must be in a scope the compiler can track. If the check is in a different function or in an async callback, the narrowing doesn't carry over. Also, if the value is reassigned after the check, the narrowing is lost.

What is the difference between ?. and ??

?. is optional chaining — it short-circuits to undefined if the value is null or undefined. ?? is nullish coalescing — it provides a fallback when the left side is null or undefined. They often appear together: obj?.prop ?? defaultValue.

Should I use the non-null assertion operator (!)?

Rarely. The ! operator tells TypeScript 'trust me, this is not null' — it doesn't check at runtime. If the value IS null, you crash. Use it only when you've just validated the value in a way TypeScript can't see, or with an external library that doesn't type correctly.

🔗 Related

← All TypeScript problems
Problem solved · TypeScript