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

SyntaxWhat It DoesReturns If Nullish
obj?.propAccess property safelyundefined
obj?.method()Call method safelyundefined
arr?.[index]Access array element safelyundefined
a?.b?.cChain multiple optional accessesundefined at first null
a?.b ?? "default"Provide fallback for undefined result"default"

Leave a Comment

Your email address will not be published. Required fields are marked *