Advanced Types - Unions, Intersections, and Type Guards

The $50,000 Payment Bug That Wasn't

It was Black Friday, 4:23 AM. I woke up to silence. No alerts. No errors. But something felt wrong.

I checked our payment processor dashboard: $127,000 in transactions. Normal for Black Friday. But then I saw the refund queue: $0.

Our refund processing had silently failed for 6 hours. The code:

// payment-processor.js - Old JavaScript
function processTransaction(transaction) {
  if (transaction.type === 'charge') {
    return chargeCard(transaction.amount, transaction.cardToken);
  } else if (transaction.type === 'refund') {
    return refundCard(transaction.amount, transaction.cardToken);
  }
  // What if transaction.type was 'authorization'?
  // Or 'void'? Or a typo like 'reund'?
  // Silently returns undefined!
}

A new transaction type had been added: 'authorization'. But this function didn't handle it. Result: 2,847 unprocessed authorizations. Customers couldn't complete purchases. Cart abandonment spiked. We estimated $50,000 in lost revenue.

The fix with TypeScript took 30 minutes:

type TransactionType = 'charge' | 'refund' | 'authorization' | 'void';

interface Transaction {
  id: string;
  type: TransactionType;
  amount: number;
  cardToken: string;
}

function processTransaction(transaction: Transaction): Promise<TransactionResult> {
  switch (transaction.type) {
    case 'charge':
      return chargeCard(transaction.amount, transaction.cardToken);
    case 'refund':
      return refundCard(transaction.amount, transaction.cardToken);
    case 'authorization':
      return authorizeCard(transaction.amount, transaction.cardToken);
    case 'void':
      return voidTransaction(transaction.id);
    default:
      // TypeScript ensures this is never reached
      const exhaustiveCheck: never = transaction.type;
      throw new Error(`Unhandled transaction type: ${exhaustiveCheck}`);
  }
}

Result: TypeScript would have caught the missing case at compile time. The bug would never have reached production.

This article covers union types, intersections, and type narrowing - the tools that prevent silent failures.


Union Types

A value that can be one of several types.

Basic Union Types

Union with Multiple Types

Union with null and undefined


Type Narrowing

Refining union types to more specific types.

typeof Type Guards

Truthiness Narrowing

Equality Narrowing

in Operator Narrowing

instanceof Narrowing


Discriminated Unions

Union types with a common literal property.

Basic Discriminated Union

Real-World Example: API Response

Exhaustiveness Checking

This saved me from the Black Friday incident. Add a new case? TypeScript forces you to handle it.


Intersection Types

Combine multiple types into one.

Basic Intersection

Intersection with Multiple Types

Extending Interfaces vs Intersections


Literal Types

Exact values as types.

String Literal Types

Number Literal Types

Boolean Literal Types

Combining Literals


Type Predicates

Custom type guards for complex narrowing.

Basic Type Predicate

Object Type Predicates

Array Type Predicates

Real-world pattern from my validation library:


Assertion Functions

Functions that assert a condition or throw.

Basic Assertion Function

Type Assertion Functions

Real-World Example


Real-World Patterns

1. Form State Management

2. Event Handling

3. Result Type Pattern


Common Mistakes I Made

1. Not Narrowing Unions

Bad:

Good:

2. Forgetting Exhaustiveness Checks

Bad:

Good:

3. Using Type Assertions Instead of Narrowing

Bad:

Good:


Your Challenge

Build a type-safe state machine for a shopping cart:


Key Takeaways

  1. Union types - value can be one of several types

  2. Type narrowing - refine unions to specific types

  3. Discriminated unions - common literal property for switching

  4. Intersection types - combine multiple types

  5. Literal types - exact values as types

  6. Type predicates - custom type guards with is

  7. Assertion functions - throw or narrow with asserts

  8. Exhaustiveness checking - ensure all cases handled


What I Learned

The Black Friday payment incident cost us $50,000 in lost revenue. The root cause? A silent failure when handling an unexpected value.

In JavaScript:

The new 'authorization' type slipped through. No errors. No warnings. Just lost sales.

In TypeScript with discriminated unions:

Add a new transaction type? TypeScript forces you to handle it. No silent failures. No lost revenue.

Union types and exhaustiveness checking turned runtime disasters into compile-time catches.

The $50,000 lesson: Make invalid states unrepresentable. Make unhandled cases impossible.


Next: Generics

In the next article, we'll explore TypeScript's generic type system. You'll learn how generics helped me build a type-safe cache that prevented a 12-hour database outage.

Last updated