Skip to Content
Course content

139: Assignment-Based Narrowing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're working in a warehouse. You have a giant conveyor belt of crates, and some of those crates contain a "Priority Shipping" manifest. If you just glance at the belt and say, "Oh, that crate over there has a manifest," and then walk over to grab it, there's a tiny chance that in the three seconds it took you to walk, a robotic arm swapped that crate for a different one. You'd reach for the manifest and find nothing but packing peanuts.

To prevent this, you do something different: you physically grab the crate and pull it off the belt. Once it's in your hands, it doesn't matter what happens to the conveyor belt. You've "locked in" the object. If you check the manifest while holding the crate, you know for a fact that the manifest is there for as long as you're holding it.

In TypeScript, assignment-based narrowing is exactly that. It's the act of taking a potentially unstable value (like a property on an object that could be changed by another function) and assigning it to a local variable to "lock" its type for the rest of the block.

The danger of the "Mutable Property"

You've probably written code like this before:


interface UserProfile {
  settings?: {
    theme: string;
  };
}

function applyTheme(user: UserProfile) {
  if (user.settings) {
    // TypeScript usually narrows this, but wait...
    console.log(user.settings.theme);
  }
}

In a simple function, this works fine. But imagine user is a global object or passed by reference, and you're calling an async function inside that if block. Between the time you checked if (user.settings) and the time you actually used user.settings.theme, some other part of your app could have set user.settings = undefined. TypeScript is smart enough to be nervous about this in complex scenarios.

Locking the type with a local constant

This is where we use assignment-based narrowing. Instead of checking the property on the object repeatedly, we move it into its own variable. I do this constantly in my own production code because it's safer and, frankly, cleaner to read.


function applyTheme(user: UserProfile) {
  const settings = user.settings; // We "grab the crate" here

  if (settings) {
    // Now, 'settings' is narrowed to { theme: string }
    // Even if user.settings is changed elsewhere, 
    // our local 'settings' variable remains unchanged.
    console.log(settings.theme); 
  }
}

By assigning user.settings to const settings, you've created a snapshot. TypeScript sees that settings is a constant. Once the if (settings) check passes, TypeScript knows that settings cannot possibly become undefined later in the block. The type is narrowed from { theme: string } | undefined to just { theme: string }.

Why this beats repeated property access

Aside from the type safety, there's a mental overhead cost to reading user.settings.theme, user.settings.fontSize, and user.settings.language over and over. By narrowing via assignment, you give the value a short, meaningful name for the duration of that logic. It tells anyone reading your code: "For the next ten lines, we are focusing specifically on these settings."




📋 Practical Task

Refactoring the Order Validation Logic

You are working on an e-commerce checkout system. You have a function that processes a shipping label, but it's currently prone to "property access" risks because the order object is mutable and passed through several middleware functions.

Below is the problematic code. Your task is to refactor the processShipping function to use assignment-based narrowing. Instead of checking order.shippingDetails multiple times, assign it to a local constant and narrow that constant.


interface ShippingDetails {
  address: string;
  zipCode: string;
  express: boolean;
}

interface Order {
  id: string;
  shippingDetails?: ShippingDetails;
}

function processShipping(order: Order) {
  // REFACTOR THIS BLOCK
  if (order.shippingDetails) {
    console.log("Shipping to: " + order.shippingDetails.address);
    console.log("Zip: " + order.shippingDetails.zipCode);
    
    if (order.shippingDetails.express) {
      console.log("Priority shipping enabled");
    }
  } else {
    console.log("No shipping details provided");
  }
}

const myOrder: Order = { id: "ORD-123", shippingDetails: { address: "123 Main St", zipCode: "10001", express: true } };
processShipping(myOrder);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.