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
137: The in Operator Narrowing
A few years ago, I was reviewing a PR for a teammate who was building a payment integration. They had a union type for payment methods—CreditCard and PayPal—and they were trying to access the cardNumber property inside a conditional. They wrote if (payment.cardNumber) { ... }, and TypeScript immediately threw a fit, complaining that cardNumber didn't exist on the PayPal type.
My teammate tried to "fix" it by casting the payment object as any just to shut the compiler up. I had to step in and explain that they were fighting the type system when they should have been collaborating with it. The problem was that a simple truthiness check doesn't always convince TypeScript that a property exists; it only checks if the value is truthy. To actually narrow the type, we needed the in operator.
Narrowing via Property Existence
The in operator is a built-in JavaScript feature that returns true if a specified property is in an object. In TypeScript, this becomes a powerful type guard. When you use in within an if statement, TypeScript understands that if the property exists, the object must be the specific member of the union that contains that property.
interface CreditCard {
type: 'card';
cardNumber: string;
expiry: string;
}
interface PayPal {
type: 'paypal';
email: string;
}
type PaymentMethod = CreditCard | PayPal;
function processPayment(method: PaymentMethod) {
if ('cardNumber' in method) {
// TypeScript knows 'method' is CreditCard here
console.log(`Charging card ${method.cardNumber}`);
} else {
// Since it's not a CreditCard, it must be PayPal
console.log(`Redirecting to ${method.email}`);
}
}
Notice how we didn't have to manually cast the type or create a complex custom guard function. The in operator does the heavy lifting by narrowing the PaymentMethod union down to the specific interface.
The Trap of Truthiness vs. Existence
You might be wondering why we can't just use if (method.cardNumber). Here is the catch: for TypeScript to allow that access, the property must exist on all members of the union, or you must have already narrowed the type. If cardNumber only exists on CreditCard, accessing it on a PaymentMethod union is technically unsafe because the object might be a PayPal instance.
Furthermore, checking for truthiness can lead to bugs if the property exists but holds a "falsy" value—like 0, an empty string, or false. The in operator doesn't care what the value is; it only cares if the key exists in the object's structure. I've seen countless bugs where a 0 value triggered an else block that was meant for "missing" data. Using in avoids that headache entirely.
📋 Practical Task
Implementing a Notification Dispatcher Guard
You are building a notification system that supports two types of alerts: EmailNotification and PushNotification. Your goal is to create a function that handles these notifications differently based on their properties.
Requirements:
- Define an interface
EmailNotificationwith propertiesemailAddress(string) andsubject(string). - Define an interface
PushNotificationwith propertiesdeviceId(string) andappId(string). - Create a union type
Notificationthat combines both. - Write a function
sendNotification(notif: Notification). - Inside the function, use the
inoperator to check ifemailAddressexists. If it does, log: "Sending email to [emailAddress]". Otherwise, log: "Sending push to [deviceId]".
// Your code hereThere are no comments for now.