TypeScript Optional Chaining
Optional chaining uses the ?. operator to safely access properties and call methods on a value that might be null or undefined. Instead of writing multiple nested null checks, one ?. stops the chain and returns undefined the moment it hits a null or undefined value — no crash, no error.
The Problem Without Optional Chaining
WITHOUT ?. — you write defensive checks at every step:
let city = user
? user.address
? user.address.city
? user.address.city
: undefined
: undefined
: undefined;
WITH ?. — one clean line:
let city = user?.address?.city;
The ?. Operator on Properties
interface Address {
street: string;
city: string;
pincode: string;
}
interface User {
name: string;
address?: Address; // address may not exist
}
let user1: User = {
name: "Sonal",
address: {
street: "12 MG Road",
city: "Bengaluru",
pincode: "560001"
}
};
let user2: User = {
name: "Arjun"
// no address provided
};
console.log(user1?.address?.city); // Bengaluru
console.log(user2?.address?.city); // undefined (no crash)
// Without ?.
// console.log(user2.address.city); // TypeError: Cannot read properties of undefined
Step-by-Step: What ?. Does
user2?.address?.city │ ├── user2 exists? YES → continue │ ├── user2.address exists? NO → STOP, return undefined │ └── (city never evaluated) Result: undefined (safe — no crash)
Optional Method Calls
?. also prevents crashes when calling a method that might not exist on an optional object.
interface Logger {
log(message: string): void;
warn(message: string): void;
}
interface AppConfig {
logger?: Logger; // logger is optional
theme: string;
}
let config: AppConfig = { theme: "dark" }; // no logger provided
config.logger?.log("App started"); // no error — safely skips
config.logger?.warn("Low memory"); // no error — safely skips
// Without ?.
// config.logger.log("..."); // TypeError: Cannot read properties of undefined
Optional Chaining with Arrays
interface Order {
id: number;
items?: string[]; // items list may be missing
}
let order1: Order = { id: 101, items: ["Book", "Pen", "Eraser"] };
let order2: Order = { id: 102 };
console.log(order1.items?.[0]); // Book
console.log(order2.items?.[0]); // undefined
console.log(order1.items?.length); // 3
console.log(order2.items?.length); // undefined
Combining ?. with ?? (Nullish Coalescing)
Pair ?. with ?? to provide a fallback value when the chain returns undefined.
interface Product {
name: string;
seller?: {
name: string;
rating?: number;
};
}
let product1: Product = {
name: "Laptop",
seller: { name: "TechMart", rating: 4.5 }
};
let product2: Product = {
name: "Pen",
seller: { name: "StatShop" } // no rating
};
let product3: Product = {
name: "Eraser"
// no seller
};
console.log(product1.seller?.rating ?? "Not rated"); // 4.5
console.log(product2.seller?.rating ?? "Not rated"); // Not rated
console.log(product3.seller?.name ?? "Unknown"); // Unknown
Nested Optional Chaining
interface Company {
name: string;
headquarters?: {
country?: {
name: string;
code: string;
};
};
}
let c1: Company = {
name: "Infosys",
headquarters: {
country: { name: "India", code: "IN" }
}
};
let c2: Company = { name: "StartupX" };
console.log(c1.headquarters?.country?.name); // India
console.log(c2.headquarters?.country?.name); // undefined
console.log(c2.headquarters?.country?.name ?? "Unknown country"); // Unknown country
Practical Example: E-Commerce Dashboard
interface Discount {
percent: number;
code: string;
}
interface CartItem {
productName: string;
price: number;
discount?: Discount;
}
interface Cart {
userId: number;
items?: CartItem[];
coupon?: {
apply(): string;
};
}
function summarizeCart(cart: Cart): void {
console.log(`User: ${cart.userId}`);
let itemCount = cart.items?.length ?? 0;
console.log(`Items: ${itemCount}`);
let firstItem = cart.items?.[0];
console.log(`First item: ${firstItem?.productName ?? "Cart is empty"}`);
let discount = cart.items?.[0]?.discount?.percent ?? 0;
console.log(`First item discount: ${discount}%`);
let couponResult = cart.coupon?.apply() ?? "No coupon applied";
console.log(`Coupon: ${couponResult}`);
}
let cart1: Cart = {
userId: 1,
items: [
{ productName: "Phone", price: 15000, discount: { percent: 10, code: "SAVE10" } },
{ productName: "Case", price: 500 }
],
coupon: { apply: () => "SAVE200 applied — ₹200 off" }
};
let cart2: Cart = { userId: 2 };
summarizeCart(cart1);
// User: 1
// Items: 2
// First item: Phone
// First item discount: 10%
// Coupon: SAVE200 applied — ₹200 off
summarizeCart(cart2);
// User: 2
// Items: 0
// First item: Cart is empty
// First item discount: 0%
// Coupon: No coupon applied
Optional Chaining Summary
| Syntax | What It Does | Returns If Nullish |
|---|---|---|
obj?.prop | Access property safely | undefined |
obj?.method() | Call method safely | undefined |
arr?.[index] | Access array element safely | undefined |
a?.b?.c | Chain multiple optional accesses | undefined at first null |
a?.b ?? "default" | Provide fallback for undefined result | "default" |
