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, totalPaymentReceived: orderId, timestamp, paymentMethod, amountOrderShipped: orderId, timestamp, trackingNumber, carrierOrderDelivered: 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
| Concept | Syntax | Use Case | |
|---|---|---|---|
| Union type | A \ | B | Multiple 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!