🔥 0
0
Lesson 6 of 10 20 min +100 XP

Union Types & Literals

Real e-commerce data isn't always a single type. Payment can be card OR UPI. Orders can be pending OR shipped OR delivered. Union types handle this elegantly.

Basic Union Types

Use | to allow multiple types:

// Can be string or number
let productId: string | number;

productId = "SKU-12345";  // ✅ Valid
productId = 12345;        // ✅ Also valid
productId = true;         // ❌ Error: boolean not assignable

// Nullable types
let couponCode: string | null = null;

if (userHasCoupon) {
  couponCode = "SAVE20";
}

Literal Types

Restrict to exact values - not just any string, but specific strings:

// Only these exact strings are allowed
type PaymentMethod = "card" | "upi" | "netbanking" | "cod";

let payment: PaymentMethod;

payment = "card";      // ✅ Valid
payment = "upi";       // ✅ Valid
payment = "paypal";    // ❌ Error: '"paypal"' is not assignable to type 'PaymentMethod'
payment = "CARD";      // ❌ Error: case-sensitive!

Number Literals

// Star ratings 1-5 only
type Rating = 1 | 2 | 3 | 4 | 5;

let productRating: Rating;

productRating = 5;   // ✅ Valid
productRating = 3;   // ✅ Valid
productRating = 0;   // ❌ Error: '0' is not assignable
productRating = 4.5; // ❌ Error: '4.5' is not assignable

---

E-commerce Status Types

Perfect for tracking order lifecycle:

type OrderStatus =
  | "pending"      // Just placed
  | "confirmed"    // Payment received
  | "processing"   // Being prepared
  | "shipped"      // On the way
  | "delivered"    // Complete
  | "cancelled"    // Cancelled
  | "refunded";    // Money returned

interface Order {
  id: string;
  items: CartItem[];
  total: number;
  status: OrderStatus;  // Can only be one of the above
}

function updateOrderStatus(order: Order, newStatus: OrderStatus): Order {
  return { ...order, status: newStatus };
}

// TypeScript prevents invalid statuses
updateOrderStatus(order, "shipped");    // ✅
updateOrderStatus(order, "in-transit"); // ❌ Error

Product Categories

type Category =
  | "electronics"
  | "clothing"
  | "home"
  | "books"
  | "sports"
  | "beauty";

interface Product {
  id: string;
  name: string;
  price: number;
  category: Category;
}

// Autocomplete works - shows all options when you type
const laptop: Product = {
  id: "SKU-001",
  name: "MacBook Pro",
  price: 1999,
  category: "electronics"  // IDE shows: electronics, clothing, home...
};

---

Discriminated Unions

Different shapes of data with a common field to tell them apart:

// Different product types need different fields
interface PhysicalProduct {
  type: "physical";      // Discriminant
  id: string;
  name: string;
  price: number;
  weight: number;        // Only physical products
  shippingCost: number;  // Only physical products
}

interface DigitalProduct {
  type: "digital";       // Discriminant
  id: string;
  name: string;
  price: number;
  downloadUrl: string;   // Only digital products
  fileSizeMB: number;    // Only digital products
}

interface SubscriptionProduct {
  type: "subscription";  // Discriminant
  id: string;
  name: string;
  price: number;
  billingCycle: "monthly" | "yearly";
  trialDays: number;
}

// Union of all product types
type Product = PhysicalProduct | DigitalProduct | SubscriptionProduct;

// TypeScript narrows the type based on 'type' field
function getProductDetails(product: Product): string {
  switch (product.type) {
    case "physical":
      // TypeScript knows: product is PhysicalProduct here
      return `Ships for ${product.shippingCost} (${product.weight}kg)`;

    case "digital":
      // TypeScript knows: product is DigitalProduct here
      return `Download: ${product.downloadUrl} (${product.fileSizeMB}MB)`;

    case "subscription":
      // TypeScript knows: product is SubscriptionProduct here
      return `${product.billingCycle} billing, ${product.trialDays} day trial`;
  }
}

---

Payment Processing Example

interface CardPayment {
  method: "card";
  cardNumber: string;
  expiryMonth: number;
  expiryYear: number;
  cvv: string;
}

interface UPIPayment {
  method: "upi";
  upiId: string;
}

interface CODPayment {
  method: "cod";
  // No additional fields needed
}

interface NetBankingPayment {
  method: "netbanking";
  bankCode: string;
}

type Payment = CardPayment | UPIPayment | CODPayment | NetBankingPayment;

function processPayment(payment: Payment, amount: number): void {
  console.log(`Processing ${amount} via ${payment.method}`);

  switch (payment.method) {
    case "card":
      console.log(`Card ending in ${payment.cardNumber.slice(-4)}`);
      break;
    case "upi":
      console.log(`UPI ID: ${payment.upiId}`);
      break;
    case "cod":
      console.log("Collect on delivery");
      break;
    case "netbanking":
      console.log(`Bank: ${payment.bankCode}`);
      break;
  }
}

// Usage
const cardPayment: Payment = {
  method: "card",
  cardNumber: "4111111111111111",
  expiryMonth: 12,
  expiryYear: 2025,
  cvv: "123"
};

processPayment(cardPayment, 99.99);

---

Combining with Interfaces

type ShippingSpeed = "standard" | "express" | "overnight";
type ProductSize = "XS" | "S" | "M" | "L" | "XL" | "XXL";

interface ShippingOption {
  speed: ShippingSpeed;
  price: number;
  estimatedDays: number;
}

interface ClothingProduct {
  id: string;
  name: string;
  price: number;
  category: "clothing";
  availableSizes: ProductSize[];
  selectedSize?: ProductSize;
}

const tshirt: ClothingProduct = {
  id: "SKU-100",
  name: "Cotton T-Shirt",
  price: 24.99,
  category: "clothing",
  availableSizes: ["S", "M", "L", "XL"],
  selectedSize: "M"
};

---

Type Challenges

Challenge 1: Order Event Types

Create a discriminated union for order events:

  • OrderPlaced: orderId, timestamp, total
  • PaymentReceived: orderId, timestamp, paymentMethod, amount
  • OrderShipped: orderId, timestamp, trackingNumber, carrier
  • OrderDelivered: orderId, timestamp, signature
Solution
interface OrderPlaced {
  type: "placed";
  orderId: string;
  timestamp: Date;
  total: number;
}

interface PaymentReceived {
  type: "payment";
  orderId: string;
  timestamp: Date;
  paymentMethod: PaymentMethod;
  amount: number;
}

interface OrderShipped {
  type: "shipped";
  orderId: string;
  timestamp: Date;
  trackingNumber: string;
  carrier: string;
}

interface OrderDelivered {
  type: "delivered";
  orderId: string;
  timestamp: Date;
  signature: string;
}

type OrderEvent = OrderPlaced | PaymentReceived | OrderShipped | OrderDelivered;

function handleOrderEvent(event: OrderEvent): void {
  switch (event.type) {
    case "placed":
      console.log(`New order: ${event.orderId}, Total: ${event.total}`);
      break;
    case "payment":
      console.log(`Payment via ${event.paymentMethod}: ${event.amount}`);
      break;
    case "shipped":
      console.log(`Shipped via ${event.carrier}: ${event.trackingNumber}`);
      break;
    case "delivered":
      console.log(`Delivered, signed by: ${event.signature}`);
      break;
  }
}

Challenge 2: Discount Types

Create union type for different discount strategies:

  • Percentage off (e.g., 20% off)
  • Fixed amount off (e.g., $10 off)
  • Buy X get Y free (e.g., buy 2 get 1 free)
Solution
interface PercentageDiscount {
  type: "percentage";
  percent: number;  // e.g., 20 for 20%
}

interface FixedDiscount {
  type: "fixed";
  amount: number;  // e.g., 10 for $10 off
}

interface BuyXGetYDiscount {
  type: "buyXgetY";
  buyQuantity: number;
  freeQuantity: number;
}

type Discount = PercentageDiscount | FixedDiscount | BuyXGetYDiscount;

function calculateDiscount(price: number, quantity: number, discount: Discount): number {
  switch (discount.type) {
    case "percentage":
      return price * quantity * (discount.percent / 100);
    case "fixed":
      return discount.amount;
    case "buyXgetY":
      const sets = Math.floor(quantity / (discount.buyQuantity + discount.freeQuantity));
      return sets * price * discount.freeQuantity;
  }
}

---

Key Takeaways

ConceptSyntaxUse Case
Union typeA \BMultiple possible types
Literal type"value"Exact values only
String literals"a" \"b"Enum-like constraints
Discriminated union{ type: "x" }Different shapes with common field

Next up: Type Narrowing - safely working with union types using conditions!

Typed Functions