TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
139: Assignment-Based Narrowing
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);
There are no comments for now.