Technology · TypeScript

TypeScript Null Safety

A reference for handling null and undefined safely in TypeScript projects.

TL;DR
  1. 01Use optional chaining (?.) to safely access nested properties on nullable values.
  2. 02Use nullish coalescing (??) to supply default values for null or undefined.
  3. 03Apply type guards to narrow a union type before accessing its properties.

Optional Chaining

  • Safely access nested properties with optional chaining.
    const user: User | null = getUser();
    
    // Without optional chaining (error if user is null)
    const name = user.profile.name; // Error
    
    // With optional chaining (safe)
    const name = user?.profile?.name; // undefined if user is null
  • Optional chaining returns undefined if any part is null or undefined.
    const city = user?.address?.city; // undefined rather than throwing
  • Chain optional method calls with ?. before the parentheses.
    const length = user?.getName?.(); // undefined if getName doesn't exist
  • Combine optional chaining with nullish coalescing for fallback values.
    const displayName = user?.profile?.displayName ?? "Anonymous";
  • Use optional chaining on array access and computed properties.
    const firstTag = post?.tags?.[0];          // undefined if tags is absent
    const value = map?.["dynamic-key"]?.trim(); // safe dynamic key access

Nullish Coalescing

  • Use ?? to provide default values only for null or undefined.
    const name = user?.name ?? "Guest";
    const count = value ?? 0;
  • Unlike ||, ?? does not replace empty strings or zero.
    const count = 0;
    
    console.log(count || 10); // 10 — wrong, zero is falsy
    console.log(count ?? 10); // 0  — correct, zero is not null
  • Use ??= to assign a default only when the variable is null or undefined.
    let config: Config | null = null;
    config ??= defaultConfig; // assigned only when null or undefined
  • Chain ?? with optional chaining for safe nested default values.
    const role = user?.permissions?.role ?? "viewer";
  • Use ?? in function parameters to handle missing arguments.
    function paginate(page: number | null, size: number | null) {
      const p = page ?? 1;
      const s = size ?? 20;
      return { page: p, size: s };
    }

Type Guards

  • Check for null before accessing properties.
    function printLength(value: string | null) {
      if (value !== null) {
        console.log(value.length); // Safe: value is string
      }
    }
  • Use typeof to guard primitive types.
    if (typeof value === "string") {
      console.log(value.toUpperCase()); // Safe: narrowed to string
    }
  • Use instanceof to narrow class instances.
    function handle(err: unknown) {
      if (err instanceof Error) {
        console.error(err.message); // Safe: narrowed to Error
      }
    }
  • Write a custom type guard function with a type predicate.
    function isUser(value: unknown): value is User {
      return typeof value === "object" && value !== null && "name" in value;
    }
    
    if (isUser(data)) {
      console.log(data.name); // Safe: narrowed to User
    }
  • Use in operator to narrow discriminated union types.
    type Cat = { meow(): void };
    type Dog = { bark(): void };
    
    function speak(animal: Cat | Dog) {
      if ("meow" in animal) animal.meow();
      else animal.bark();
    }

Optional Types

  • Mark properties as optional with ? to allow undefined.
    interface User {
      name: string;
      email?: string;       // can be undefined
      phone: string | null; // can be null (must be explicit)
    }
  • Optional properties don't need to be included when constructing an object.
    const user: User = {
      name: "Alice"
      // email is optional — safe to omit
    };
  • Distinguish optional (?) from nullable (| null) for precise types.
    interface Post {
      title: string;
      subtitle?: string;       // missing or undefined
      deletedAt: Date | null;  // always present, but can be null
    }
  • Mark function parameters optional to make them skippable.
    function greet(name: string, title?: string): string {
      return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
    }
    
    greet("Alice");           // OK
    greet("Alice", "Dr.");    // OK
  • Use Required to remove optional modifiers when needed.
    interface Options {
      timeout?: number;
      retries?: number;
    }
    
    function runWithDefaults(opts: Required<Options>) {
      console.log(opts.timeout, opts.retries); // both guaranteed present
    }

Non-Null Assertion

  • Use ! to tell TypeScript a value is not null or undefined.
    const value = getValue();
    const length = value!.length; // Assert value is not null
  • Prefer an explicit null check over the non-null assertion.
    // Good: check first — safe at runtime
    if (value !== null) {
      console.log(value.length);
    }
    
    // Avoid: assertion without check — can throw at runtime
    console.log(value!.length);
  • Use ! on DOM queries when you know the element exists.
    const input = document.getElementById("email")!;
    // Safe only if the element is guaranteed in the HTML
  • Avoid ! inside library code — callers may not share your assumptions.
    // Better: surface the possibility of null in the return type
    function findUser(id: number): User | null {
      return db.users.find(u => u.id === id) ?? null;
    }
  • Use optional chaining as a safer alternative to non-null assertion.
    // Assertion — crashes if null
    element!.classList.add("active");
    
    // Optional chaining — silently skips if null
    element?.classList.add("active");

Tip: Enable strictNullChecks in tsconfig.json to catch null safety issues at compile time.

Warning: Non-null assertions (!) bypass type checking — use them only when you're certain the value is not null.

TypeScript Generics ConstraintsTypeScript Performance Tips