TypeScript Type Guards

Published: (April 6, 2026 at 07:25 AM EDT)
5 min read
Source: Dev.to

Source: Dev.to

When you’re building a payment system, “close enough” isn’t good enough

A single undefined value or a mismatched object property can be the difference between a successful transaction and a frustrated customer (or a lost sale).

TypeScript’s Type Guards are your first line of defense. They let you narrow broad, uncertain types into specific ones that you can safely interact with. In this guide we’ll build a mini‑payment processor and learn how to use Type Guards to make it crash‑proof.

1. The Problem: The “Silent Failures” of JavaScript

Imagine you have a function that processes different types of payment responses. In plain JavaScript you might write something like this:

function processResponse(response) {
  // If response is a 'Success' object, it has a 'transactionId'
  // If it's an 'Error' object, it has a 'message'
  console.log("Payment successful! ID: " + response.transactionId);
}

// What if the API returned an error?
processResponse({ message: "Insufficient funds" });
// Output: "Payment successful! ID: undefined"

JavaScript doesn’t complain; it just gives you undefined. This is a silent failure. TypeScript can help catch it—if we know how to narrow the types.

2. Built‑in Guards: The Foundation

TypeScript provides built‑in operators that perform runtime checks. When TypeScript sees these checks, it narrows the type for the rest of that code block.

typeof (Checking primitives)

In our payment app, an amount might be a number or a string (if it comes from a form input).

function formatAmount(amount: string | number) {
  if (typeof amount === "string") {
    // TypeScript knows `amount` is a string here.
    // We can safely call string‑specific methods.
    return parseFloat(amount).toFixed(2);
  }
  // Here TypeScript knows `amount` must be a number.
  return amount.toFixed(2);
}

instanceof (Checking classes)

Suppose you have different classes for CreditCard and GiftCard payments. Each has its own verification logic.

class CreditCard {
  verifyCVV() {
    return true;
  }
}

class GiftCard {
  checkBalance() {
    return 50.0;
  }
}

function verifyPayment(method: CreditCard | GiftCard) {
  if (method instanceof CreditCard) {
    // Safe to call CreditCard methods
    method.verifyCVV();
  } else {
    // Safe to call GiftCard methods
    method.checkBalance();
  }
}

3. Custom Type Predicates: The “Bouncer”

Sometimes simple checks aren’t enough. You might want a reusable function to check if a payment is complete. That’s where a Custom Type Predicate comes in.

interface Payment {
  id: string;
  status: "pending" | "completed" | "failed";
}

interface CompletedPayment extends Payment {
  status: "completed";
  confirmedAt: Date;
}

/* This is our Custom Type Predicate */
function isCompleted(payment: Payment): payment is CompletedPayment {
  return payment.status === "completed";
}

const myPayment: Payment = { id: "123", status: "completed" };

if (isCompleted(myPayment)) {
  // TypeScript now knows `myPayment` has a `confirmedAt` property!
  console.log(myPayment.confirmedAt);
}

4. Discriminated Unions: The Gold Standard

If you only learn one pattern from this article, make it this one. By adding a single, common property (a discriminator) to your types, you gain 100 % type safety and perfect IDE autocomplete.

interface CardPayment {
  type: "card"; // Discriminator
  lastFour: string;
}

interface PayPalPayment {
  type: "paypal"; // Discriminator
  email: string;
}

type PaymentMethod = CardPayment | PayPalPayment;

function getReceipt(method: PaymentMethod) {
  switch (method.type) {
    case "card":
      return `Charged card ending in ${method.lastFour}`;
    case "paypal":
      return `Charged PayPal account: ${method.email}`;
  }
}

Why this is a super‑power: If you later add a Bitcoin payment type but forget to update the switch, TypeScript will flag an error immediately. It’s like having a senior developer looking over your shoulder.

5. Anti‑Patterns: The “Before and After”

As a beginner you might be tempted to use shortcuts to silence the red error lines. Let’s see why you shouldn’t.

The “Unsafe Assertion”

Before (the “dirty” way):

// Bypassing safety with `as any`
const receipt = (payment as any).cardNumber;
// If `payment` is actually a Bank Transfer, this is `undefined`.

After (the “guard” way):

if ("cardNumber" in payment) {
  const receipt = payment.cardNumber; // Safe and verified
}

Using the in operator acts as a shield when dealing with unpredictable data from external APIs.

Summary: Your Type Guard Decision Tree

Not sure which guard to use? Follow this checklist:

SituationGuard to Use
Simple primitive (string, number, boolean, etc.)typeof
Object created with new MyClass()instanceof
Types share a common property like type or statusDiscriminated Unions (best for complex logic)
Checking for a specific property on a “messy” objectin operator
You want a reusable function to clean up if statementsCustom Type Predicate

Use the right guard, and your payment processor will be far less likely to crash – and far more likely to keep customers happy.

Predicate (is)

Conclusion

Type Guards bridge the gap between TypeScript’s strict rules and JavaScript’s flexible reality. By implementing these patterns in your payment system, you’re not just writing code—you’re building a reliable, predictable engine that handles every edge case with ease.

Happy coding!

Reference:
Mastering TypeScript Type Guards — Better Stack Community

0 views
Back to Blog

Related posts

Read more »

Spread vs Rest Operators in JavaScript

!Cover image for Spread vs Rest Operators in JavaScripthttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%...

Map and Set in JavaScript

What Is a Map? A Map is a collection of key‑value pairs, similar to an object, but with several improvements: - Keys can be any type objects, functions, primit...