TypeScript Type Guards
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:
| Situation | Guard to Use |
|---|---|
| Simple primitive (string, number, boolean, etc.) | typeof |
Object created with new MyClass() | instanceof |
Types share a common property like type or status | Discriminated Unions (best for complex logic) |
| Checking for a specific property on a “messy” object | in operator |
You want a reusable function to clean up if statements | Custom 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