TypeScript Null & Undefined

Handling null and undefined values safely

🛡️ Null & Undefined in TypeScript

TypeScript provides strict null checking to prevent common runtime errors. With strictNullChecks enabled, null and undefined are distinct types that must be explicitly handled, making your code safer and more predictable by catching potential null reference errors.


// Strict null checking
let name: string = "Alice";
// name = null; // Error!
let optionalName: string | null = null; // OK
                                    

Understanding Null and Undefined

Null

Intentional absence of value

let user: User | null = null;

Undefined

Variable not yet assigned

let age: number | undefined;
🔒

Strict Checks

Enable strictNullChecks

"strictNullChecks": true

Type Guards

Check before using values

if (value !== null) {
  // Safe to use
}

🔹 Null vs Undefined

Understanding the difference:

// Undefined - variable declared but not assigned
let username: string | undefined;
console.log(username); // undefined

// Null - intentionally empty value
let user: User | null = null;
console.log(user); // null

// Both can be used together
let data: string | null | undefined;
data = "Hello";
data = null;
data = undefined;

🔹 Strict Null Checks

Enable strict null checking in tsconfig.json:

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

🔸 Without Strict Null Checks

// strictNullChecks: false
let name: string = "Alice";
name = null;      // No error
name = undefined; // No error

function greet(name: string) {
    console.log(name.toUpperCase());
}
greet(null); // Runtime error!

🔸 With Strict Null Checks

// strictNullChecks: true
let name: string = "Alice";
// name = null;      // Error!
// name = undefined; // Error!

// Must explicitly allow null/undefined
let optionalName: string | null = null; // OK

function greet(name: string | null) {
    if (name !== null) {
        console.log(name.toUpperCase()); // Safe
    }
}

🔹 Optional Properties

Use ? to make properties optional:

interface User {
    id: number;
    name: string;
    email?: string;        // Optional: string | undefined
    phone?: string | null; // Optional and nullable
}

const user1: User = {
    id: 1,
    name: "Alice"
    // email is undefined (not provided)
};

const user2: User = {
    id: 2,
    name: "Bob",
    email: "[email protected]"
};

const user3: User = {
    id: 3,
    name: "Charlie",
    email: undefined,
    phone: null
};

🔹 Type Guards for Null Checks

Safely check for null and undefined:

🔸 If Statement

function printName(name: string | null | undefined) {
    if (name) {
        console.log(name.toUpperCase());
    } else {
        console.log("No name provided");
    }
}

printName("Alice");     // ALICE
printName(null);        // No name provided
printName(undefined);   // No name provided

🔸 Strict Equality

function processValue(value: string | null | undefined) {
    if (value !== null && value !== undefined) {
        console.log(`Value: ${value}`);
    }
    
    // Or check for null specifically
    if (value === null) {
        console.log("Value is null");
    }
    
    // Or check for undefined specifically
    if (value === undefined) {
        console.log("Value is undefined");
    }
}

🔹 Non-null Assertion Operator (!)

Tell TypeScript a value is not null/undefined:

function getUser(): User | null {
    return { id: 1, name: "Alice" };
}

const user = getUser();

// Without assertion - Error
// console.log(user.name); // Error: Object is possibly 'null'

// With assertion - No error (use carefully!)
console.log(user!.name); // OK, but risky

// Better approach - Type guard
if (user !== null) {
    console.log(user.name); // Safe
}

⚠️ Warning:

Use the non-null assertion operator (!) sparingly. It bypasses TypeScript's safety checks and can lead to runtime errors if the value is actually null or undefined.

🔹 Nullish Coalescing (??)

Provide default values for null or undefined:

// Nullish coalescing operator (??)
let username: string | null = null;
let displayName = username ?? "Guest";
console.log(displayName); // "Guest"

username = "Alice";
displayName = username ?? "Guest";
console.log(displayName); // "Alice"

// Difference from || operator
let count: number | null = 0;
let result1 = count || 10;  // 10 (0 is falsy)
let result2 = count ?? 10;  // 0 (0 is not null/undefined)

console.log(result1); // 10
console.log(result2); // 0

🔹 Optional Chaining (?.)

Safely access nested properties:

interface Address {
    street: string;
    city: string;
}

interface User {
    name: string;
    address?: Address;
}

const user1: User = {
    name: "Alice",
    address: { street: "123 Main St", city: "Boston" }
};

const user2: User = {
    name: "Bob"
};

// Without optional chaining
// console.log(user2.address.city); // Error!

// With optional chaining
console.log(user1.address?.city); // "Boston"
console.log(user2.address?.city); // undefined

// Combine with nullish coalescing
const city = user2.address?.city ?? "Unknown";
console.log(city); // "Unknown"

🔹 Definite Assignment Assertion

Tell TypeScript a variable will be assigned:

class UserService {
    private user!: User; // Definite assignment assertion
    
    constructor() {
        this.initialize();
    }
    
    private initialize() {
        this.user = { id: 1, name: "Alice" };
    }
    
    getUser(): User {
        return this.user; // No error
    }
}

// Without assertion
class BadService {
    private user: User; // Error: Property 'user' has no initializer
    
    constructor() {
        this.initialize();
    }
    
    private initialize() {
        this.user = { id: 1, name: "Alice" };
    }
}

🔹 Best Practices

  • Enable strictNullChecks: Always use strict null checking
  • Use Type Guards: Check for null/undefined before using values
  • Prefer Optional Chaining: Use ?. for safe property access
  • Use Nullish Coalescing: Provide defaults with ??
  • Avoid ! Operator: Use type guards instead of assertions
  • Be Explicit: Clearly indicate when values can be null/undefined

🔹 Practical Example

interface Product {
    id: number;
    name: string;
    price: number;
    discount?: number;
}

class ShoppingCart {
    private items: Product[] = [];
    
    addItem(product: Product | null): void {
        if (product === null) {
            console.log("Cannot add null product");
            return;
        }
        this.items.push(product);
    }
    
    getTotal(): number {
        return this.items.reduce((total, item) => {
            const discount = item.discount ?? 0;
            return total + (item.price - discount);
        }, 0);
    }
    
    findItem(id: number): Product | undefined {
        return this.items.find(item => item.id === id);
    }
}

const cart = new ShoppingCart();
cart.addItem({ id: 1, name: "Laptop", price: 1000, discount: 100 });
cart.addItem({ id: 2, name: "Mouse", price: 50 });
cart.addItem(null); // Cannot add null product

console.log(`Total: $${cart.getTotal()}`); // Total: $950

const item = cart.findItem(1);
console.log(item?.name ?? "Item not found"); // Laptop

🧠 Test Your Knowledge

What operator safely accesses nested properties?