Skip to Content
Course content

137: The in Operator Narrowing

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

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 EmailNotification with properties emailAddress (string) and subject (string).
  • Define an interface PushNotification with properties deviceId (string) and appId (string).
  • Create a union type Notification that combines both.
  • Write a function sendNotification(notif: Notification).
  • Inside the function, use the in operator to check if emailAddress exists. If it does, log: "Sending email to [emailAddress]". Otherwise, log: "Sending push to [deviceId]".
// Your code here
Rating
0 0

There are no comments for now.

to be the first to leave a comment.