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
135: Truthiness Narrowing
You've already seen how we can use typeof or instanceof to tell TypeScript what a variable is. But in the real world, we spend a huge amount of our time just checking if something exists. This is where truthiness narrowing comes in.
TypeScript is smart enough to know that if you put a variable into an if statement, you're essentially asking: "Is this value truthy?" If the check passes, TypeScript automatically narrows the type by removing all the falsy possibilities—like null, undefined, 0, NaN, and empty strings.
Building a dynamic user greeting
Let's imagine we're building a small piece of a profile page. We have a user object where the displayName is optional. If it's there, we want to use it; otherwise, we'll fall back to "Guest".
type User = {
displayName?: string;
id: string;
};
function greetUser(user: User) {
if (user.displayName) {
// Inside this block, TypeScript knows displayName is definitely a string.
// It has narrowed the type from 'string | undefined' to just 'string'.
console.log(`Welcome back, ${user.displayName.toUpperCase()}!`);
} else {
console.log("Welcome, Guest!");
}
}
This is the "magic" of truthiness. I didn't have to write if (user.displayName !== undefined). I just checked the value itself, and TypeScript followed my logic.
Where I usually trip up: The empty string trap
Here is where I've bitten myself more than once. Truthiness is convenient, but it's a blunt instrument. It doesn't just filter out null or undefined; it filters out everything falsy.
Let's say I want to allow users to have an empty string as their status, and I only want to show a "No status set" message if the value is actually null or undefined. Watch what happens when I try to use truthiness narrowing here:
function formatStatus(status: string | null) {
if (status) {
return `Status: ${status}`;
}
return "No status set";
}
console.log(formatStatus("Coding...")); // "Status: Coding..."
console.log(formatStatus(null)); // "No status set"
console.log(formatStatus("")); // "No status set" <--- Wait, this is wrong!
I just realized my mistake. By using if (status), I accidentally treated the empty string "" as if it were null. In many apps, an empty string is a valid value (meaning the user explicitly cleared their status), while null means they've never touched it. My "convenient" narrowing just created a bug.
Switching to explicit nullish checks
When truthiness is too broad, I stop relying on it and move to a more surgical approach. If I only care about null and undefined, I'll check for those specifically, or use the nullish coalescing operator.
function formatStatusFixed(status: string | null) {
// By checking explicitly against null, I leave the empty string alone.
if (status !== null) {
// Now status is narrowed to just 'string', including ""
return `Status: ${status}`;
}
return "No status set";
}
console.log(formatStatusFixed("")); // "Status: " (Correct!)
The takeaway here is balance. Use truthiness narrowing when you genuinely want to exclude all falsy values. But the moment you're dealing with numbers (where 0 is a valid value) or strings (where "" is valid), be explicit. Your future self will thank you when you're debugging a weird edge case at 4 PM on a Friday.
📋 Practical Task
Fixing the Zero-Value Bug in a Scoreboard
You are building a game scoreboard. The currentScore can be a number or null (if the game hasn't started). The current implementation uses truthiness narrowing, which is causing a bug: when a player has a score of 0, the app incorrectly tells them they haven't started the game yet.
Fix the displayScore function so that it correctly displays "Score: 0" instead of "Game not started".
type GameState = {
currentScore: number | null;
};
function displayScore(game: GameState) {
// FIX THIS LINE:
if (game.currentScore) {
return `Score: ${game.currentScore}`;
}
return "Game not started";
}
const player1 = { currentScore: 10 };
const player2 = { currentScore: 0 };
const player3 = { currentScore: null };
console.log(displayScore(player1)); // Expected: "Score: 10"
console.log(displayScore(player2)); // Currently: "Game not started" | Expected: "Score: 0"
console.log(displayScore(player3)); // Expected: "Game not started"There are no comments for now.