9.7 Assignment Narrowing
Right, so you’ve got a variable that could be one of a few types. The big question is: how do you convince TypeScript’s ever-vigilant type checker that right now, at this specific line of code, it’s actually a specific one? One of the simplest and most common ways this happens is through the utterly mundane act of assignment. It’s so straightforward you might miss its power.
When you assign a new value directly to a variable, TypeScript looks at that new value, checks its type, and says, “Well, alright then, I guess we’re doing this.” It promptly narrows the variable’s type from whatever it was before to the type of the new value. This is Assignment Narrowing.
Let’s start with a classic union example.
let userId: string | number = getUserIdFromSomewhere();
// Right now, `userId` is string | number
userId = "abc-123";
// Now, TypeScript knows it's a string. It's been narrowed.
console.log(userId.toUpperCase()); // All good!
userId = 42;
// And now, it's proudly a number.
console.log(userId.toFixed(2)); // Also good.
See? No fancy typeof checks, no type predicates. Just you telling the variable what its new reality is, and TypeScript accepting it. It’s the programming equivalent of “new phone, who dis?”
How It Works Under the Hood
This isn’t magic; it’s a deliberate design choice for soundness and practicality. TypeScript’s type system tracks the flow of your code (this is called control flow analysis). An assignment is a major signpost in that flow. The type checker reasons that after that line, any previous value (and by extension, its type) is gone, replaced by the new one. It effectively says, “I no longer need to consider the previous possibilities from the union for this variable.” It’s a hard reset for the variable’s type at that point in your code.
The Const Caveat (This is Important)
Here’s where people get tripped up. Assignment narrowing works beautifully on let and var variables because their whole purpose is to be reassigned. But what about const?
const id: string | number = "abc-123";
If you hover over id in your editor, you’ll see its type is… "abc-123". Not string. Wait, what?
This is TypeScript being brilliantly clever. A const can’t be reassigned. Therefore, the only value this variable will ever have is the literal value "abc-123". It narrows it all the way down to that literal type. It doesn’t need to keep the wider string type around because you can’t change it later. This is a form of literal narrowing, but it’s triggered by the assignment to a const. For a union, it narrows to the specific member of the union that matches the assigned value.
The Re-Assignment Gotcha
Assignment narrowing is powerful, but it’s also a bit of a blunt instrument. It completely overwrites the previous type. This can lead to headaches if you’re not careful, especially with mutable data structures.
let data: string | { value: string } = getData();
data = "Loading..."; // Narrowed to string. Fine.
// ... some code ...
data = { value: "Complete" }; // Narrowed to { value: string }. Also fine.
// But what if you mutate the object instead of reassigning?
let config: { path: string } | null = { path: "/tmp" };
// This is not a reassignment of 'config', it's a mutation of its property!
config.path = "/home"; // đ„ Error: Object is possibly 'null'.
This error makes perfect sense. You never reassigned config itself, so from TypeScript’s perspective, it’s still { path: string } | null. The assignment narrowing never happened. You need a type guard to check config isn’t null before you dive into mutating its properties. This is a classic pitfallâconfusing mutation for reassignment.
Best Practice: Prefer Const
This whole re-assignment mess is a great argument for using const by default and only reaching for let when you genuinely need mutability. With a const, the type is narrowed immediately and permanently to the most specific type possible. You get maximum type safety with zero ambiguity. You avoid entire classes of errors related to accidental reassignment or misunderstanding the current type state. It’s one of the simplest ways to make your code more robust. Use let strategically, not habitually.