+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 20 of 354

๐ŸŒˆ Union and Intersection Types: Combining Types Effectively

Master union and intersection types in TypeScript to create flexible, powerful type systems for real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Strong understanding of TypeScript basics ๐Ÿ“
  • Familiarity with interfaces and types โšก
  • Knowledge of type aliases ๐Ÿ’ป

What you'll learn

  • Create flexible types with unions ๐ŸŽฏ
  • Combine types using intersections ๐Ÿ—๏ธ
  • Use discriminated unions effectively ๐Ÿ”
  • Build powerful type systems โœจ

๐ŸŽฏ Introduction

Welcome to the colorful world of union and intersection types in TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to combine types like a master chef combines ingredients to create something amazing.

Think of union types as โ€œORโ€ operations (this OR that) ๐Ÿ•|๐Ÿ” and intersection types as โ€œANDโ€ operations (this AND that) ๐Ÿ•&๐ŸŸ. Together, theyโ€™re your Swiss Army knife for creating flexible, precise type systems that can handle any scenario!

By the end of this tutorial, youโ€™ll be combining types like a TypeScript wizard, creating APIs that are both flexible and type-safe! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Union and Intersection Types

๐Ÿค” What Are Union Types?

Union types are like a restaurant menu with options ๐Ÿฝ๏ธ - you can choose this OR that! They represent values that can be one of several types.

The syntax uses the pipe symbol |:

type Meal = "pizza" | "burger" | "salad";
type PaymentMethod = "card" | "cash" | "crypto";

๐Ÿ’ก What Are Intersection Types?

Intersection types are like a combo meal ๐Ÿฑ - you get this AND that! They combine multiple types into one.

The syntax uses the ampersand &:

type Person = { name: string } & { age: number };
// Result: { name: string; age: number }

๐ŸŽฏ Why Use Them?

Hereโ€™s why these types are game-changers:

  1. Flexible APIs ๐ŸŒˆ: Handle multiple input types gracefully
  2. Precise Modeling ๐ŸŽฏ: Represent real-world scenarios accurately
  3. Type Safety ๐Ÿ›ก๏ธ: Catch errors while allowing flexibility
  4. Better IntelliSense ๐Ÿ’ป: IDE understands all possibilities
  5. Composability ๐Ÿ—๏ธ: Build complex types from simple ones

Real-world example: A payment system ๐Ÿ’ณ that accepts credit cards, PayPal, or cryptocurrency - thatโ€™s a union type in action!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Union Types Basics

Letโ€™s start with union types:

// ๐ŸŒˆ Basic union types
type Status = "idle" | "loading" | "success" | "error";
type Size = "small" | "medium" | "large";

// ๐ŸŽจ Union with different types
type StringOrNumber = string | number;
type BooleanOrNull = boolean | null;

// ๐Ÿš€ Using unions
let value: StringOrNumber = "hello";
value = 42; // Also valid!
// value = true; // Error! boolean not in union

// ๐ŸŽฏ Function with union parameters
function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else {
    return value.toFixed(2);
  }
}

console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(3.14159)); // "3.14"

// ๐Ÿท๏ธ Union with objects
type SuccessResponse = {
  status: "success";
  data: any;
};

type ErrorResponse = {
  status: "error";
  message: string;
  code: number;
};

type ApiResponse = SuccessResponse | ErrorResponse;

// ๐ŸŽฎ Using the union
function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    console.log("โœ… Data:", response.data);
  } else {
    console.log("โŒ Error:", response.message);
  }
}

๐ŸŽจ Intersection Types Basics

Now letโ€™s explore intersection types:

// ๐Ÿค Basic intersection
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// Result: { name: string; age: number }

const person: Person = {
  name: "Alice",
  age: 30
};

// ๐Ÿ—๏ธ Combining interfaces
interface Timestamp {
  createdAt: Date;
  updatedAt: Date;
}

interface Author {
  authorId: string;
  authorName: string;
}

type Article = {
  title: string;
  content: string;
} & Timestamp & Author;

const article: Article = {
  title: "TypeScript Magic โœจ",
  content: "Union and intersection types are awesome!",
  createdAt: new Date(),
  updatedAt: new Date(),
  authorId: "123",
  authorName: "Alice"
};

// ๐Ÿš€ Function intersections
type Logger = {
  log: (message: string) => void;
};

type Counter = {
  count: number;
  increment: () => void;
};

type LoggingCounter = Logger & Counter;

const loggingCounter: LoggingCounter = {
  count: 0,
  increment() {
    this.count++;
    this.log(`Count is now ${this.count}`);
  },
  log(message) {
    console.log(`๐Ÿ“Š ${message}`);
  }
};

๐ŸŽฏ Combining Unions and Intersections

// ๐ŸŽ† Complex type combinations
type Admin = {
  role: "admin";
  permissions: string[];
};

type User = {
  role: "user";
  subscription: "free" | "premium";
};

type Guest = {
  role: "guest";
  sessionId: string;
};

// Union of different user types
type AnyUser = Admin | User | Guest;

// Add common properties with intersection
type AuthenticatedUser = AnyUser & {
  lastLogin: Date;
  isActive: boolean;
};

// ๐ŸŽฎ Using discriminated unions
function getWelcomeMessage(user: AnyUser): string {
  switch (user.role) {
    case "admin":
      return `๐Ÿ‘‘ Welcome Admin! You have ${user.permissions.length} permissions`;
    case "user":
      return `๐Ÿ‘‹ Welcome! You have a ${user.subscription} account`;
    case "guest":
      return `๐Ÿ‘€ Welcome Guest! Session: ${user.sessionId}`;
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce System

Letโ€™s build a flexible payment system using unions:

// ๐Ÿ’ณ Payment method types
type CreditCard = {
  type: "credit-card";
  cardNumber: string;
  cvv: string;
  expiryDate: string;
  emoji: "๐Ÿ’ณ";
};

type PayPal = {
  type: "paypal";
  email: string;
  password: string;
  emoji: "๐Ÿ’ต";
};

type Cryptocurrency = {
  type: "crypto";
  walletAddress: string;
  currency: "BTC" | "ETH" | "USDT";
  emoji: "๐Ÿช™";
};

type BankTransfer = {
  type: "bank-transfer";
  accountNumber: string;
  routingNumber: string;
  emoji: "๐Ÿฆ";
};

// Union of all payment methods
type PaymentMethod = CreditCard | PayPal | Cryptocurrency | BankTransfer;

// ๐Ÿ›๏ธ Order with payment
interface Order {
  id: string;
  items: Array<{ name: string; price: number; quantity: number }>;
  total: number;
}

type PaidOrder = Order & {
  paymentMethod: PaymentMethod;
  paidAt: Date;
  transactionId: string;
};

// ๐ŸŽฏ Process payment based on type
class PaymentProcessor {
  processPayment(order: Order, payment: PaymentMethod): PaidOrder | null {
    console.log(`\n${payment.emoji} Processing ${payment.type} payment...`);
    
    switch (payment.type) {
      case "credit-card":
        console.log(`๐Ÿ’ณ Charging card ending in ${payment.cardNumber.slice(-4)}`);
        break;
        
      case "paypal":
        console.log(`๐Ÿ’ต PayPal account: ${payment.email}`);
        break;
        
      case "crypto":
        console.log(`๐Ÿช™ Sending ${payment.currency} request to ${payment.walletAddress.slice(0, 10)}...`);
        break;
        
      case "bank-transfer":
        console.log(`๐Ÿฆ Initiating bank transfer from account ${payment.accountNumber}`);
        break;
    }
    
    // Simulate payment processing
    const success = Math.random() > 0.1; // 90% success rate
    
    if (success) {
      console.log("โœ… Payment successful!");
      return {
        ...order,
        paymentMethod: payment,
        paidAt: new Date(),
        transactionId: `TXN-${Date.now()}`
      };
    } else {
      console.log("โŒ Payment failed!");
      return null;
    }
  }
  
  // ๐Ÿ“‹ Get payment summary
  getPaymentSummary(payment: PaymentMethod): string {
    switch (payment.type) {
      case "credit-card":
        return `Card ****${payment.cardNumber.slice(-4)}`;
      case "paypal":
        return `PayPal (${payment.email})`;
      case "crypto":
        return `${payment.currency} (${payment.walletAddress.slice(0, 6)}...)`;
      case "bank-transfer":
        return `Bank Transfer (****${payment.accountNumber.slice(-4)})`;
    }
  }
}

// ๐ŸŽฎ Usage
const processor = new PaymentProcessor();
const order: Order = {
  id: "ORD-123",
  items: [
    { name: "TypeScript Book ๐Ÿ“˜", price: 39.99, quantity: 1 },
    { name: "Coffee โ˜•", price: 4.99, quantity: 3 }
  ],
  total: 54.96
};

// Try different payment methods
const creditCardPayment: CreditCard = {
  type: "credit-card",
  cardNumber: "1234567812345678",
  cvv: "123",
  expiryDate: "12/25",
  emoji: "๐Ÿ’ณ"
};

const cryptoPayment: Cryptocurrency = {
  type: "crypto",
  walletAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E123",
  currency: "ETH",
  emoji: "๐Ÿช™"
};

processor.processPayment(order, creditCardPayment);
processor.processPayment(order, cryptoPayment);

๐ŸŽจ Example 2: Form Validation System

Letโ€™s create a flexible validation system:

// ๐Ÿ” Validation rule types
type RequiredRule = {
  type: "required";
  message: string;
};

type MinLengthRule = {
  type: "minLength";
  value: number;
  message: string;
};

type MaxLengthRule = {
  type: "maxLength";
  value: number;
  message: string;
};

type PatternRule = {
  type: "pattern";
  regex: RegExp;
  message: string;
};

type EmailRule = {
  type: "email";
  message: string;
};

// Union of all validation rules
type ValidationRule = RequiredRule | MinLengthRule | MaxLengthRule | PatternRule | EmailRule;

// ๐Ÿท๏ธ Field with validation
type FormField<T = string> = {
  name: string;
  label: string;
  value: T;
  rules: ValidationRule[];
  errors: string[];
  touched: boolean;
};

// ๐Ÿ—๏ธ Form state
type FormState = {
  fields: Record<string, FormField>;
  isValid: boolean;
  isSubmitting: boolean;
};

// ๐ŸŽฏ Validation system
class FormValidator {
  validateField(field: FormField): string[] {
    const errors: string[] = [];
    
    for (const rule of field.rules) {
      const error = this.validateRule(field.value, rule);
      if (error) {
        errors.push(error);
      }
    }
    
    return errors;
  }
  
  private validateRule(value: string, rule: ValidationRule): string | null {
    switch (rule.type) {
      case "required":
        return value.trim() === "" ? rule.message : null;
        
      case "minLength":
        return value.length < rule.value ? rule.message : null;
        
      case "maxLength":
        return value.length > rule.value ? rule.message : null;
        
      case "pattern":
        return !rule.regex.test(value) ? rule.message : null;
        
      case "email":
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return !emailRegex.test(value) ? rule.message : null;
    }
  }
  
  // ๐Ÿ“Š Validate entire form
  validateForm(form: FormState): FormState {
    const updatedFields: Record<string, FormField> = {};
    let isValid = true;
    
    for (const [name, field] of Object.entries(form.fields)) {
      const errors = this.validateField(field);
      updatedFields[name] = { ...field, errors };
      
      if (errors.length > 0) {
        isValid = false;
      }
    }
    
    return {
      ...form,
      fields: updatedFields,
      isValid
    };
  }
}

// ๐ŸŽฎ Example usage
const validator = new FormValidator();

const loginForm: FormState = {
  fields: {
    email: {
      name: "email",
      label: "Email Address ๐Ÿ“ง",
      value: "user@example",
      rules: [
        { type: "required", message: "Email is required" },
        { type: "email", message: "Please enter a valid email" }
      ],
      errors: [],
      touched: true
    },
    password: {
      name: "password",
      label: "Password ๐Ÿ”’",
      value: "123",
      rules: [
        { type: "required", message: "Password is required" },
        { type: "minLength", value: 8, message: "Password must be at least 8 characters" },
        { type: "pattern", regex: /(?=.*[A-Z])(?=.*[0-9])/, message: "Password must contain uppercase and numbers" }
      ],
      errors: [],
      touched: true
    }
  },
  isValid: false,
  isSubmitting: false
};

const validatedForm = validator.validateForm(loginForm);

// Display results
for (const [name, field] of Object.entries(validatedForm.fields)) {
  console.log(`\n${field.label}:`);
  console.log(`  Value: "${field.value}"`);
  if (field.errors.length > 0) {
    console.log(`  โŒ Errors:`);
    field.errors.forEach(error => console.log(`    - ${error}`));
  } else {
    console.log(`  โœ… Valid!`);
  }
}

console.log(`\n๐Ÿท๏ธ Form is ${validatedForm.isValid ? 'valid โœ…' : 'invalid โŒ'}`);

๐ŸŽฎ Example 3: Game State Management

// ๐ŸŽฎ Game entities with intersections
type Position = { x: number; y: number };
type Velocity = { vx: number; vy: number };
type Health = { health: number; maxHealth: number };
type Sprite = { sprite: string; color: string };

// Base entity
type Entity = Position & Sprite & {
  id: string;
  type: string;
};

// Different entity types
type Player = Entity & Health & Velocity & {
  type: "player";
  score: number;
  lives: number;
  powerups: string[];
};

type Enemy = Entity & Health & Velocity & {
  type: "enemy";
  damage: number;
  behavior: "aggressive" | "defensive" | "patrol";
};

type PowerUp = Entity & {
  type: "powerup";
  effect: "speed" | "shield" | "multishot";
  duration: number;
};

type Obstacle = Entity & {
  type: "obstacle";
  solid: boolean;
  destructible: boolean;
};

// Union of all game objects
type GameObject = Player | Enemy | PowerUp | Obstacle;

// ๐ŸŽฎ Game engine
class GameEngine {
  private entities: Map<string, GameObject> = new Map();
  
  // ๐ŸŽฏ Spawn entities
  spawn<T extends GameObject>(entity: T): void {
    this.entities.set(entity.id, entity);
    console.log(`โœจ Spawned ${entity.type} at (${entity.x}, ${entity.y})`);
  }
  
  // ๐Ÿš€ Update game state
  update(): void {
    for (const entity of this.entities.values()) {
      this.updateEntity(entity);
    }
  }
  
  private updateEntity(entity: GameObject): void {
    switch (entity.type) {
      case "player":
        this.updatePlayer(entity);
        break;
      case "enemy":
        this.updateEnemy(entity);
        break;
      case "powerup":
        this.updatePowerUp(entity);
        break;
      case "obstacle":
        // Obstacles don't update
        break;
    }
  }
  
  private updatePlayer(player: Player): void {
    player.x += player.vx;
    player.y += player.vy;
    console.log(`๐ŸŽฎ Player at (${player.x}, ${player.y}) - Health: ${player.health}/${player.maxHealth}`);
  }
  
  private updateEnemy(enemy: Enemy): void {
    // AI behavior based on type
    switch (enemy.behavior) {
      case "aggressive":
        console.log(`๐Ÿ‘ฟ Aggressive enemy chasing player!`);
        break;
      case "defensive":
        console.log(`๐Ÿ›ก๏ธ Defensive enemy holding position`);
        break;
      case "patrol":
        console.log(`๐Ÿ‘€ Patrolling enemy moving on route`);
        break;
    }
  }
  
  private updatePowerUp(powerup: PowerUp): void {
    console.log(`โœจ ${powerup.effect} powerup available!`);
  }
  
  // ๐Ÿ’ฅ Check collisions
  checkCollision(a: Entity, b: Entity): boolean {
    const distance = Math.sqrt(
      Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
    );
    return distance < 10; // Simple radius check
  }
}

// ๐ŸŽฎ Create game
const game = new GameEngine();

// Spawn entities
game.spawn<Player>({
  id: "player-1",
  type: "player",
  x: 50,
  y: 50,
  vx: 0,
  vy: 0,
  health: 100,
  maxHealth: 100,
  score: 0,
  lives: 3,
  powerups: [],
  sprite: "๐Ÿง‘โ€๐Ÿš€",
  color: "blue"
});

game.spawn<Enemy>({
  id: "enemy-1",
  type: "enemy",
  x: 200,
  y: 100,
  vx: -1,
  vy: 0,
  health: 50,
  maxHealth: 50,
  damage: 10,
  behavior: "aggressive",
  sprite: "๐Ÿ‘พ",
  color: "red"
});

game.spawn<PowerUp>({
  id: "powerup-1",
  type: "powerup",
  x: 150,
  y: 150,
  effect: "shield",
  duration: 5000,
  sprite: "๐Ÿ›ก๏ธ",
  color: "gold"
});

// Run game loop
game.update();

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Discriminated Unions

The most powerful pattern with unions:

// ๐ŸŽฏ Discriminated union for state management
type LoadingState = {
  status: "loading";
  message: string;
};

type SuccessState<T> = {
  status: "success";
  data: T;
  timestamp: Date;
};

type ErrorState = {
  status: "error";
  error: Error;
  code: number;
  retry: () => void;
};

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

// ๐ŸŽจ Type-safe state handler
function handleAsyncState<T>(state: AsyncState<T>): void {
  switch (state.status) {
    case "loading":
      console.log(`โณ ${state.message}`);
      break;
      
    case "success":
      console.log(`โœ… Success at ${state.timestamp.toLocaleTimeString()}`);
      console.log(`๐Ÿ“ฆ Data:`, state.data);
      break;
      
    case "error":
      console.log(`โŒ Error ${state.code}: ${state.error.message}`);
      console.log(`๐Ÿ”„ Retrying...`);
      state.retry();
      break;
  }
}

// ๐Ÿš€ Advanced: Exhaustive checking
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function processState<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return "Success!";
    case "error":
      return "Error!";
    default:
      // TypeScript ensures all cases are handled
      return assertNever(state);
  }
}

๐Ÿ—๏ธ Advanced Intersection Patterns

// ๐Ÿงฌ Mixin pattern with intersections
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixins
function Timestamped<T extends Constructor>(Base: T) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
    
    touch() {
      this.updatedAt = new Date();
    }
  };
}

function Taggable<T extends Constructor>(Base: T) {
  return class extends Base {
    tags: string[] = [];
    
    addTag(tag: string) {
      this.tags.push(tag);
      console.log(`๐Ÿท๏ธ Added tag: ${tag}`);
    }
  };
}

// Base class
class Article {
  constructor(public title: string, public content: string) {}
}

// Apply mixins
const TimestampedTaggableArticle = Taggable(Timestamped(Article));

// Use the mixed class
const article = new TimestampedTaggableArticle(
  "TypeScript Mixins",
  "Intersection types enable powerful patterns!"
);

article.addTag("typescript");
article.addTag("advanced");
article.touch();

console.log(`๐Ÿ“ Article: ${article.title}`);
console.log(`๐Ÿท๏ธ Tags: ${article.tags.join(", ")}`);
console.log(`๐Ÿ“… Updated: ${article.updatedAt.toLocaleString()}`);

๐ŸŽจ Type Guards with Unions

// ๐Ÿ” Custom type guards
type Fish = {
  type: "fish";
  swim: () => void;
  fins: number;
};

type Bird = {
  type: "bird";
  fly: () => void;
  wings: number;
};

type Dog = {
  type: "dog";
  bark: () => void;
  breed: string;
};

type Animal = Fish | Bird | Dog;

// Type guard functions
function isFish(animal: Animal): animal is Fish {
  return animal.type === "fish";
}

function isBird(animal: Animal): animal is Bird {
  return animal.type === "bird";
}

function isDog(animal: Animal): animal is Dog {
  return animal.type === "dog";
}

// ๐ŸŽฏ Use type guards
function interactWithAnimal(animal: Animal): void {
  console.log(`\n๐Ÿพ Interacting with ${animal.type}`);
  
  if (isFish(animal)) {
    console.log(`๐ŸŸ Fish has ${animal.fins} fins`);
    animal.swim();
  } else if (isBird(animal)) {
    console.log(`๐Ÿฆ… Bird has ${animal.wings} wings`);
    animal.fly();
  } else if (isDog(animal)) {
    console.log(`๐Ÿ• Dog breed: ${animal.breed}`);
    animal.bark();
  }
}

// ๐ŸŽฎ Create animals
const animals: Animal[] = [
  {
    type: "fish",
    fins: 3,
    swim: () => console.log("๐ŸŠ Swimming!")
  },
  {
    type: "bird",
    wings: 2,
    fly: () => console.log("๐Ÿฆ… Flying high!")
  },
  {
    type: "dog",
    breed: "Golden Retriever",
    bark: () => console.log("๐Ÿถ Woof woof!")
  }
];

animals.forEach(interactWithAnimal);

๐Ÿš€ Conditional Types with Unions

// ๐Ÿงฌ Extract and exclude utilities
type Status = "idle" | "loading" | "success" | "error";

// Extract specific types
type LoadingStates = Extract<Status, "idle" | "loading">; // "idle" | "loading"
type CompleteStates = Extract<Status, "success" | "error">; // "success" | "error"

// Exclude types
type NonErrorStates = Exclude<Status, "error">; // "idle" | "loading" | "success"
type ActiveStates = Exclude<Status, "idle">; // "loading" | "success" | "error"

// ๐ŸŽฏ Distributive conditional types
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<string | number>; // boolean (true | false)

// ๐ŸŽจ Filter union members
type FilterStrings<T> = T extends string ? T : never;
type StringMembers = FilterStrings<string | number | boolean | "hello" | "world">;
// Result: string | "hello" | "world"

// ๐Ÿ—บ๏ธ Map over union types
type EventTypes = "click" | "focus" | "blur";
type EventHandlers = {
  [K in EventTypes as `on${Capitalize<K>}`]: (event: Event) => void;
};
// Result: { onClick: ..., onFocus: ..., onBlur: ... }

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Union Type Property Access

// โŒ Wrong - can't access properties not in all union members
type Cat = { type: "cat"; meow: () => void };
type Dog = { type: "dog"; bark: () => void };
type Pet = Cat | Dog;

function petSound(pet: Pet) {
  // pet.bark(); // Error! 'bark' doesn't exist on Cat
  // pet.meow(); // Error! 'meow' doesn't exist on Dog
}

// โœ… Correct - use type guards
function petSoundGood(pet: Pet) {
  if (pet.type === "dog") {
    pet.bark(); // TypeScript knows it's a Dog
  } else {
    pet.meow(); // TypeScript knows it's a Cat
  }
}

๐Ÿคฏ Pitfall 2: Intersection Type Conflicts

// โŒ Problem - conflicting property types
type A = { value: string };
type B = { value: number };
// type C = A & B; // value is 'never' - can't be string AND number!

// โœ… Solution 1 - use union for the property
type A2 = { value: string | number };
type B2 = { value: number };
type C2 = A2 & B2; // value is number (more specific)

// โœ… Solution 2 - different property names
type A3 = { textValue: string };
type B3 = { numValue: number };
type C3 = A3 & B3; // Both properties exist

๐Ÿ˜ต Pitfall 3: Excessive Union Types

// โŒ Too many union members - hard to handle
type Status = "idle" | "loading" | "loaded" | "reloading" | 
              "error" | "retrying" | "success" | "partial" | 
              "cancelled" | "timeout" | "pending";

// โœ… Better - group related states
type LoadState = "idle" | "loading" | "loaded";
type ErrorState = "error" | "timeout";
type SuccessState = "success" | "partial";

type BetterStatus = {
  load: LoadState;
  result?: ErrorState | SuccessState;
  isRetrying?: boolean;
};

๐Ÿค” Pitfall 4: Wrong Type Guard Assumptions

// โŒ Unsafe type guard
function isString(value: unknown): value is string {
  return typeof value === "object"; // Wrong check!
}

// โœ… Correct type guard
function isStringCorrect(value: unknown): value is string {
  return typeof value === "string";
}

// ๐ŸŽฏ Multiple checks for complex types
interface User {
  name: string;
  age: number;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "age" in value &&
    typeof (value as User).name === "string" &&
    typeof (value as User).age === "number"
  );
}

๐Ÿ˜ฌ Pitfall 5: Forgetting Distributivity

// โš ๏ธ Conditional types distribute over unions
type Wrap<T> = T extends string ? [T] : T;

type Test1 = Wrap<string | number>;
// Result: [string] | number (NOT [string | number])

// ๐Ÿ”ง To prevent distribution, wrap in a tuple
type WrapNonDistributive<T> = [T] extends [string] ? [T] : T;

type Test2 = WrapNonDistributive<string | number>;
// Result: string | number (no wrapping)

// ๐ŸŽฏ When you want distribution
type StringifyAll<T> = T extends any ? `${T & string}` : never;
type Stringified = StringifyAll<"a" | "b" | "c">; // "a" | "b" | "c"

๐Ÿ› ๏ธ Best Practices

1. ๐ŸŽฏ Use Discriminated Unions

// โœ… Good - easy to distinguish
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: Error };

function handleResult<T>(result: Result<T>) {
  if (result.success) {
    console.log("Data:", result.data);
  } else {
    console.log("Error:", result.error);
  }
}

2. ๐Ÿ“ Name Union Types Clearly

// โœ… Good - descriptive names
type PaymentStatus = "pending" | "processing" | "completed" | "failed";
type UserRole = "admin" | "moderator" | "user" | "guest";

// โŒ Avoid - generic names
type Status = "a" | "b" | "c";
type Type = 1 | 2 | 3;

3. ๐Ÿ›ก๏ธ Prefer Unions Over Enums

// โœ… Union types - simpler and more flexible
type Direction = "north" | "south" | "east" | "west";

// Use with confidence
const move = (direction: Direction) => {
  console.log(`Moving ${direction}`);
};

4. ๐ŸŽจ Keep Intersections Simple

// โœ… Good - clear composition
type Timestamps = { createdAt: Date; updatedAt: Date };
type Author = { authorId: string; authorName: string };
type Post = { title: string; content: string } & Timestamps & Author;

// โŒ Avoid - too many intersections
type Monster = A & B & C & D & E & F; // Hard to understand

5. โœจ Use Type Guards Liberally

// โœ… Create reusable type guards
const isError = <T>(result: T | Error): result is Error => {
  return result instanceof Error;
};

const hasProperty = <T, K extends string>(
  obj: T,
  key: K
): obj is T & Record<K, unknown> => {
  return obj != null && key in obj;
};

// Use throughout your code
if (isError(result)) {
  console.error(result.message);
} else {
  processData(result);
}

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Notification System

Create a flexible notification system using unions and intersections:

๐Ÿ“‹ Requirements:

  • โœ… Multiple notification types (email, SMS, push, in-app)
  • ๐Ÿท๏ธ Priority levels and scheduling
  • ๐Ÿ‘ค User preferences and filtering
  • ๐Ÿ“… Delivery tracking and retries
  • ๐ŸŽจ Rich content support

๐Ÿš€ Bonus Points:

  • Add notification templates
  • Implement batching
  • Create analytics

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Flexible notification system with unions and intersections

// Base notification properties
type NotificationBase = {
  id: string;
  userId: string;
  priority: "low" | "normal" | "high" | "urgent";
  scheduledFor?: Date;
  expiresAt?: Date;
  metadata?: Record<string, any>;
};

// Delivery tracking
type DeliveryInfo = {
  attempts: number;
  lastAttempt?: Date;
  deliveredAt?: Date;
  error?: string;
};

// Different notification channels
type EmailNotification = NotificationBase & {
  type: "email";
  to: string;
  subject: string;
  body: string;
  attachments?: Array<{ name: string; url: string }>;
  replyTo?: string;
};

type SMSNotification = NotificationBase & {
  type: "sms";
  phoneNumber: string;
  message: string;
  maxLength?: number;
};

type PushNotification = NotificationBase & {
  type: "push";
  title: string;
  body: string;
  icon?: string;
  badge?: number;
  data?: Record<string, any>;
  actions?: Array<{ id: string; title: string; icon?: string }>;
};

type InAppNotification = NotificationBase & {
  type: "in-app";
  title: string;
  message: string;
  actionUrl?: string;
  imageUrl?: string;
  persistent: boolean;
};

// Union of all notification types
type Notification = EmailNotification | SMSNotification | PushNotification | InAppNotification;

// Notification with delivery status
type TrackedNotification = Notification & DeliveryInfo;

// User preferences
type UserPreferences = {
  channels: Array<Notification["type"]>;
  quiet_hours?: { start: string; end: string };
  blockedCategories: string[];
  maxPerDay?: number;
};

// ๐Ÿ”” Notification system
class NotificationSystem {
  private notifications: Map<string, TrackedNotification> = new Map();
  private userPreferences: Map<string, UserPreferences> = new Map();
  
  // ๐Ÿ“จ Send notification
  async send(notification: Notification): Promise<boolean> {
    console.log(`\n๐Ÿ“จ Sending ${notification.type} notification...`);
    
    // Check user preferences
    const prefs = this.userPreferences.get(notification.userId);
    if (!this.canSend(notification, prefs)) {
      console.log("๐Ÿšซ Blocked by user preferences");
      return false;
    }
    
    // Add delivery tracking
    const tracked: TrackedNotification = {
      ...notification,
      attempts: 0,
      lastAttempt: new Date()
    };
    
    this.notifications.set(notification.id, tracked);
    
    // Process based on type
    return this.processNotification(tracked);
  }
  
  // ๐ŸŽฏ Process different notification types
  private async processNotification(notification: TrackedNotification): Promise<boolean> {
    notification.attempts++;
    
    try {
      switch (notification.type) {
        case "email":
          console.log(`๐Ÿ“ง Sending email to ${notification.to}`);
          console.log(`   Subject: ${notification.subject}`);
          if (notification.attachments?.length) {
            console.log(`   ๐Ÿ“Ž ${notification.attachments.length} attachments`);
          }
          break;
          
        case "sms":
          console.log(`๐Ÿ“ฑ Sending SMS to ${notification.phoneNumber}`);
          console.log(`   Message (${notification.message.length} chars): ${notification.message}`);
          break;
          
        case "push":
          console.log(`๐Ÿ”” Sending push notification`);
          console.log(`   Title: ${notification.title}`);
          if (notification.actions?.length) {
            console.log(`   ๐ŸŽฏ ${notification.actions.length} actions available`);
          }
          break;
          
        case "in-app":
          console.log(`๐Ÿ’ฌ Showing in-app notification`);
          console.log(`   Title: ${notification.title}`);
          console.log(`   Persistent: ${notification.persistent ? "Yes" : "No"}`);
          break;
      }
      
      // Simulate delivery
      const success = Math.random() > 0.1;
      
      if (success) {
        notification.deliveredAt = new Date();
        console.log("โœ… Delivered successfully!");
        return true;
      } else {
        throw new Error("Delivery failed");
      }
    } catch (error) {
      notification.error = (error as Error).message;
      console.log(`โŒ Failed: ${notification.error}`);
      
      // Retry logic
      if (notification.attempts < 3) {
        console.log(`๐Ÿ”„ Will retry (attempt ${notification.attempts}/3)`);
        setTimeout(() => this.processNotification(notification), 1000 * notification.attempts);
      }
      
      return false;
    }
  }
  
  // ๐Ÿ”’ Check user preferences
  private canSend(notification: Notification, prefs?: UserPreferences): boolean {
    if (!prefs) return true;
    
    // Check channel preference
    if (!prefs.channels.includes(notification.type)) {
      return false;
    }
    
    // Check quiet hours
    if (prefs.quiet_hours) {
      const now = new Date();
      const hour = now.getHours();
      const start = parseInt(prefs.quiet_hours.start);
      const end = parseInt(prefs.quiet_hours.end);
      
      if (hour >= start && hour < end) {
        console.log("๐ŸŒ™ In quiet hours");
        return notification.priority === "urgent";
      }
    }
    
    return true;
  }
  
  // ๐Ÿ‘ค Set user preferences
  setUserPreferences(userId: string, prefs: UserPreferences): void {
    this.userPreferences.set(userId, prefs);
    console.log(`๐Ÿ”ง Updated preferences for user ${userId}`);
  }
  
  // ๐Ÿ“Š Get statistics
  getStats(): void {
    const stats = {
      total: this.notifications.size,
      delivered: 0,
      failed: 0,
      pending: 0,
      byType: {} as Record<string, number>
    };
    
    for (const notification of this.notifications.values()) {
      if (notification.deliveredAt) stats.delivered++;
      else if (notification.error && notification.attempts >= 3) stats.failed++;
      else stats.pending++;
      
      stats.byType[notification.type] = (stats.byType[notification.type] || 0) + 1;
    }
    
    console.log("\n๐Ÿ“Š Notification Statistics:");
    console.log(`๐Ÿ“จ Total: ${stats.total}`);
    console.log(`โœ… Delivered: ${stats.delivered}`);
    console.log(`โŒ Failed: ${stats.failed}`);
    console.log(`โณ Pending: ${stats.pending}`);
    console.log(`๐Ÿท๏ธ By Type:`, stats.byType);
  }
}

// ๐ŸŽฎ Test the system
const notificationSystem = new NotificationSystem();

// Set user preferences
notificationSystem.setUserPreferences("user-123", {
  channels: ["email", "push", "in-app"],
  quiet_hours: { start: "22", end: "8" },
  blockedCategories: ["marketing"],
  maxPerDay: 10
});

// Send various notifications
notificationSystem.send({
  id: "notif-1",
  userId: "user-123",
  type: "email",
  to: "[email protected]",
  subject: "Welcome to TypeScript! ๐ŸŽ‰",
  body: "Thanks for joining our TypeScript course!",
  priority: "normal",
  attachments: [
    { name: "guide.pdf", url: "https://example.com/guide.pdf" }
  ]
});

notificationSystem.send({
  id: "notif-2",
  userId: "user-123",
  type: "push",
  title: "New Achievement! ๐Ÿ†",
  body: "You've completed 10 lessons",
  badge: 1,
  priority: "normal",
  actions: [
    { id: "view", title: "View Achievement", icon: "๐Ÿ‘€" },
    { id: "share", title: "Share", icon: "๐Ÿ“ค" }
  ]
});

notificationSystem.send({
  id: "notif-3",
  userId: "user-123",
  type: "sms",
  phoneNumber: "+1234567890",
  message: "Your verification code is 123456",
  priority: "urgent"
});

notificationSystem.send({
  id: "notif-4",
  userId: "user-123",
  type: "in-app",
  title: "Daily Tip ๐Ÿ’ก",
  message: "Did you know? TypeScript has amazing union types!",
  actionUrl: "/tips/unions",
  persistent: false,
  priority: "low"
});

// Show statistics
setTimeout(() => {
  notificationSystem.getStats();
}, 2000);

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered union and intersection types! Hereโ€™s what you can now do:

  • โœ… Create flexible types with unions (this OR that) ๐Ÿ’ช
  • โœ… Combine types with intersections (this AND that) ๐Ÿ›ก๏ธ
  • โœ… Use discriminated unions for type-safe state ๐ŸŽฏ
  • โœ… Build complex type systems from simple parts ๐Ÿ›
  • โœ… Write APIs that are both flexible and safe! ๐Ÿš€

Remember: Union types give flexibility, intersection types give power. Together, theyโ€™re unstoppable! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered union and intersection types!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Complete the notification system exercise
  2. ๐Ÿ—๏ธ Refactor existing code to use discriminated unions
  3. ๐Ÿ“š Move on to our next tutorial: Type Assertions and Type Guards
  4. ๐ŸŒŸ Build a state machine using union types!

Remember: These patterns are everywhere in real TypeScript code. The more you practice, the more natural they become! ๐Ÿš€

Happy coding! ๐ŸŽ‰๐Ÿš€โœจ