+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 94 of 355

๐ŸŽฏ Discriminated Unions: Pattern Matching Magic

Master discriminated unions in TypeScript with exhaustive pattern matching, type-safe state machines, and real-world applications ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

Prerequisites

  • Union and intersection types ๐Ÿ”—
  • Type guards and type assertions ๐Ÿ›ก๏ธ
  • Basic understanding of enums ๐Ÿ“š

What you'll learn

  • Create powerful discriminated unions for type safety ๐ŸŽฏ
  • Implement exhaustive pattern matching ๐Ÿ—๏ธ
  • Build type-safe state machines and APIs ๐Ÿ›
  • Debug union type issues like a pro โœจ

๐ŸŽฏ Introduction

Welcome to the fascinating world of discriminated unions! ๐ŸŽ‰ In this guide, weโ€™ll explore one of TypeScriptโ€™s most powerful features for creating type-safe, expressive code that eliminates entire classes of runtime errors.

Youโ€™ll discover how discriminated unions can transform complex conditional logic into elegant, bulletproof patterns. Whether youโ€™re building state machines ๐Ÿค–, API responses ๐ŸŒ, or complex data structures ๐Ÿ“š, discriminated unions provide unmatched type safety and developer experience.

By the end of this tutorial, youโ€™ll be crafting sophisticated pattern matching systems that make your code both safer and more readable! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Discriminated Unions

๐Ÿค” What is a Discriminated Union?

A discriminated union is like a sophisticated traffic light system ๐Ÿšฆ. Each โ€œstateโ€ has a unique identifier (the discriminant) that tells you exactly which type youโ€™re dealing with, plus additional data specific to that state.

In TypeScript terms, a discriminated union combines multiple types where each type has a common property (the discriminant) with a literal value that uniquely identifies it. This means you can:

  • โœจ Eliminate โ€œimpossible statesโ€ - no more invalid combinations
  • ๐Ÿš€ Get exhaustive checking - TypeScript ensures you handle all cases
  • ๐Ÿ›ก๏ธ Achieve perfect type narrowing - access properties with complete safety

๐Ÿ’ก Why Use Discriminated Unions?

Hereโ€™s why developers love discriminated unions:

  1. Exhaustive Pattern Matching ๐Ÿ”’: TypeScript ensures you handle every case
  2. Impossible States Made Impossible ๐Ÿ’ป: Prevent invalid state combinations
  3. Self-Documenting Code ๐Ÿ“–: The structure tells the story
  4. Bulletproof Refactoring ๐Ÿ”ง: Add new cases and get compile errors where you need to update

Real-world example: Imagine building an API response system ๐Ÿ›’. With discriminated unions, you can represent success, loading, and error states in a way that makes it impossible to accidentally access error messages when youโ€™re in a success state!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a classic discriminated union:

// ๐Ÿšฆ Traffic Light States
type TrafficLight = 
  | { type: 'red'; timeLeft: number; emoji: '๐Ÿ”ด' }
  | { type: 'yellow'; timeLeft: number; emoji: '๐ŸŸก' }
  | { type: 'green'; timeLeft: number; emoji: '๐ŸŸข' };

// ๐ŸŽฏ Pattern matching function
function getAction(light: TrafficLight): string {
  switch (light.type) { // ๐Ÿ‘ˆ TypeScript knows the discriminant!
    case 'red':
      return `Stop! ${light.emoji} Wait ${light.timeLeft}s`;
    case 'yellow':
      return `Caution! ${light.emoji} ${light.timeLeft}s left`;
    case 'green':
      return `Go! ${light.emoji} ${light.timeLeft}s remaining`;
    // ๐ŸŽ‰ No default case needed - all cases handled!
  }
}

๐Ÿ’ก Explanation: Notice how type is our discriminant property! TypeScript automatically narrows the type in each case, giving us perfect autocomplete and type safety.

๐ŸŽฏ Common Patterns

Here are the essential discriminated union patterns:

// ๐Ÿ—๏ธ Pattern 1: API Response States
type ApiResponse<T> = 
  | { status: 'loading'; progress?: number }
  | { status: 'success'; data: T; timestamp: Date }
  | { status: 'error'; error: string; code: number };

// ๐ŸŽจ Pattern 2: Form Validation
type ValidationResult = 
  | { valid: true; value: string }
  | { valid: false; errors: string[]; originalValue: string };

// ๐Ÿ”„ Pattern 3: State Machine Events
type PlayerAction = 
  | { type: 'PLAY'; song: string }
  | { type: 'PAUSE' }
  | { type: 'STOP' }
  | { type: 'SKIP'; direction: 'forward' | 'backward' };

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Order States

Letโ€™s build a real-world order management system:

// ๐Ÿ“ฆ Order states with different data for each state
type Order = 
  | { 
      status: 'pending'; 
      items: string[]; 
      estimatedTotal: number;
      customerEmail: string;
    }
  | { 
      status: 'confirmed'; 
      orderId: string;
      items: string[];
      total: number;
      paymentMethod: string;
      customerEmail: string;
    }
  | { 
      status: 'shipped'; 
      orderId: string;
      trackingNumber: string;
      estimatedDelivery: Date;
      carrier: string;
    }
  | { 
      status: 'delivered'; 
      orderId: string;
      deliveredAt: Date;
      signedBy?: string;
    }
  | { 
      status: 'cancelled'; 
      orderId?: string;
      reason: string;
      refundAmount?: number;
    };

// ๐ŸŽฏ Order processor with exhaustive matching
class OrderProcessor {
  processOrder(order: Order): string {
    switch (order.status) {
      case 'pending':
        // ๐Ÿ” TypeScript knows we have customerEmail and estimatedTotal
        return `โณ Order pending for ${order.customerEmail}. Estimated: $${order.estimatedTotal}`;
      
      case 'confirmed':
        // ๐Ÿ’ณ TypeScript knows we have orderId and paymentMethod
        return `โœ… Order ${order.orderId} confirmed! Paid via ${order.paymentMethod}`;
      
      case 'shipped':
        // ๐Ÿšš TypeScript knows we have trackingNumber and carrier
        return `๐Ÿš› Order ${order.orderId} shipped via ${order.carrier}. Tracking: ${order.trackingNumber}`;
      
      case 'delivered':
        // ๐Ÿ“ซ TypeScript knows we have deliveredAt
        const signInfo = order.signedBy ? ` Signed by: ${order.signedBy}` : '';
        return `๐Ÿ“ฆ Order ${order.orderId} delivered on ${order.deliveredAt.toDateString()}.${signInfo}`;
      
      case 'cancelled':
        // โŒ TypeScript knows we have reason and optional refundAmount
        const refund = order.refundAmount ? ` Refund: $${order.refundAmount}` : '';
        return `โŒ Order cancelled. Reason: ${order.reason}.${refund}`;
        
      // ๐ŸŽ‰ No default needed - TypeScript ensures all cases handled!
    }
  }
  
  // ๐Ÿ”„ State transitions with validation
  canTransitionTo(order: Order, newStatus: Order['status']): boolean {
    switch (order.status) {
      case 'pending':
        return ['confirmed', 'cancelled'].includes(newStatus);
      case 'confirmed':
        return ['shipped', 'cancelled'].includes(newStatus);
      case 'shipped':
        return ['delivered'].includes(newStatus);
      case 'delivered':
      case 'cancelled':
        return false; // ๐Ÿ”’ Final states
    }
  }
}

๐ŸŽฏ Try it yourself: Add an โ€˜on-holdโ€™ status and update the transition logic!

๐ŸŽฎ Example 2: Game Combat System

Letโ€™s create a type-safe combat system:

// โš”๏ธ Combat actions with different requirements
type CombatAction = 
  | { type: 'attack'; weaponId: string; targetId: string; damage: number }
  | { type: 'defend'; shieldValue: number; duration: number }
  | { type: 'heal'; amount: number; targetId?: string }
  | { type: 'cast_spell'; spellId: string; manaCost: number; targets: string[] }
  | { type: 'flee'; escapeChance: number; destination?: string };

// ๐Ÿ† Combat result states
type CombatResult = 
  | { outcome: 'success'; damage?: number; healing?: number; effect?: string }
  | { outcome: 'failure'; reason: string; penaltyApplied?: boolean }
  | { outcome: 'critical'; multiplier: number; damage: number; specialEffect: string }
  | { outcome: 'blocked'; blockedDamage: number; counterAttack?: boolean };

class CombatEngine {
  executeAction(action: CombatAction): CombatResult {
    switch (action.type) {
      case 'attack':
        // ๐Ÿ—ก๏ธ TypeScript knows we have weaponId, targetId, and damage
        const isCritical = Math.random() < 0.15;
        if (isCritical) {
          return {
            outcome: 'critical',
            multiplier: 2.5,
            damage: action.damage * 2.5,
            specialEffect: '๐Ÿ’ฅ Critical Strike!'
          };
        }
        return { outcome: 'success', damage: action.damage };
      
      case 'defend':
        // ๐Ÿ›ก๏ธ TypeScript knows we have shieldValue and duration
        return {
          outcome: 'success',
          effect: `๐Ÿ›ก๏ธ Defense increased by ${action.shieldValue} for ${action.duration}s`
        };
      
      case 'heal':
        // ๐Ÿ’š TypeScript knows we have amount and optional targetId
        const target = action.targetId ? 'ally' : 'self';
        return {
          outcome: 'success',
          healing: action.amount,
          effect: `๐Ÿ’š Healed ${target} for ${action.amount} HP`
        };
      
      case 'cast_spell':
        // ๐Ÿช„ TypeScript knows we have spellId, manaCost, and targets array
        if (action.targets.length === 0) {
          return { outcome: 'failure', reason: 'No targets selected' };
        }
        return {
          outcome: 'success',
          effect: `๐Ÿช„ Cast spell ${action.spellId} on ${action.targets.length} targets`
        };
      
      case 'flee':
        // ๐Ÿƒ TypeScript knows we have escapeChance and optional destination
        const escaped = Math.random() < action.escapeChance;
        return escaped 
          ? { outcome: 'success', effect: '๐Ÿƒ Escaped successfully!' }
          : { outcome: 'failure', reason: 'Failed to escape', penaltyApplied: true };
    }
  }
}

๐Ÿš€ Advanced Concepts

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

When youโ€™re ready to level up, try generic discriminated unions:

// ๐ŸŽฏ Generic Result type for any operation
type Result<T, E = string> = 
  | { success: true; data: T; timestamp: Date }
  | { success: false; error: E; retryable: boolean };

// ๐Ÿช„ Using with different data types
type UserResult = Result<{ id: string; name: string; email: string }, 'USER_NOT_FOUND' | 'INVALID_CREDENTIALS'>;
type ApiResult<T> = Result<T, { code: number; message: string; details?: unknown }>;

// ๐Ÿ”ง Generic handler function
function handleResult<T, E>(result: Result<T, E>): string {
  if (result.success) {
    return `โœ… Success! Data received at ${result.timestamp.toISOString()}`;
  } else {
    const retry = result.retryable ? ' (Retryable)' : ' (Fatal)';
    return `โŒ Error: ${JSON.stringify(result.error)}${retry}`;
  }
}

๐Ÿ—๏ธ Nested Discriminated Unions

For the brave developers who need complex state management:

// ๐Ÿš€ Nested discriminated unions for complex workflows
type PaymentMethod = 
  | { type: 'credit_card'; cardNumber: string; cvv: string; expiryDate: string }
  | { type: 'paypal'; email: string; accountId: string }
  | { type: 'bank_transfer'; routingNumber: string; accountNumber: string };

type PaymentState = 
  | { 
      status: 'selecting_method'; 
      availableMethods: PaymentMethod['type'][]; 
    }
  | { 
      status: 'processing'; 
      method: PaymentMethod; 
      transactionId: string;
      startedAt: Date;
    }
  | { 
      status: 'completed'; 
      method: PaymentMethod; 
      transactionId: string;
      completedAt: Date;
      receiptUrl: string;
    }
  | { 
      status: 'failed'; 
      method: PaymentMethod; 
      transactionId: string;
      error: string;
      retryCount: number;
    };

// ๐ŸŽฏ Exhaustive processing with nested pattern matching
function processPayment(state: PaymentState): string {
  switch (state.status) {
    case 'selecting_method':
      return `๐Ÿ›’ Choose from: ${state.availableMethods.join(', ')}`;
    
    case 'processing':
      // ๐Ÿ” We can pattern match on the nested union too!
      const methodDesc = (() => {
        switch (state.method.type) {
          case 'credit_card':
            return `๐Ÿ’ณ Card ending in ${state.method.cardNumber.slice(-4)}`;
          case 'paypal':
            return `๐Ÿ’ฐ PayPal (${state.method.email})`;
          case 'bank_transfer':
            return `๐Ÿฆ Bank transfer (${state.method.routingNumber})`;
        }
      })();
      return `โณ Processing payment via ${methodDesc} (${state.transactionId})`;
    
    case 'completed':
      return `โœ… Payment successful! Receipt: ${state.receiptUrl}`;
    
    case 'failed':
      const retryMsg = state.retryCount > 0 ? ` (Attempt ${state.retryCount + 1})` : '';
      return `โŒ Payment failed: ${state.error}${retryMsg}`;
  }
}

๐ŸŽญ Exhaustiveness Checking with never

Ensure compile-time completeness with the never type:

// ๐Ÿ›ก๏ธ Helper function to ensure exhaustive matching
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}

// ๐ŸŽฏ Function that MUST handle all cases
function processOrderStatus(order: Order): string {
  switch (order.status) {
    case 'pending':
      return 'Processing...';
    case 'confirmed':
      return 'Confirmed!';
    case 'shipped':
      return 'On the way!';
    case 'delivered':
      return 'Delivered!';
    case 'cancelled':
      return 'Cancelled';
    default:
      // ๐Ÿšจ TypeScript error if we miss any cases!
      return assertNever(order); 
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Forgetting the Discriminant

// โŒ Wrong way - no common discriminant property!
type BadUnion = 
  | { name: string; age: number }
  | { title: string; duration: number };

// TypeScript can't tell these apart! ๐Ÿ˜ฐ
function processBadUnion(item: BadUnion) {
  // ๐Ÿ’ฅ How do we know which properties are available?
  // TypeScript only gives us properties common to ALL types
}

// โœ… Correct way - always include a discriminant!
type GoodUnion = 
  | { type: 'person'; name: string; age: number }
  | { type: 'media'; title: string; duration: number };

function processGoodUnion(item: GoodUnion) {
  switch (item.type) {
    case 'person':
      console.log(`${item.name} is ${item.age} years old`); // โœ… Perfect!
      break;
    case 'media':
      console.log(`${item.title} lasts ${item.duration} minutes`); // โœ… Perfect!
      break;
  }
}

๐Ÿคฏ Pitfall 2: Using Enums as Discriminants

// โŒ Dangerous - enums can be tricky with discriminated unions
enum Status {
  LOADING = 'loading',
  SUCCESS = 'success',
  ERROR = 'error'
}

type ProblematicResponse = 
  | { status: Status.LOADING; progress: number }
  | { status: Status.SUCCESS; data: string }
  | { status: Status.ERROR; error: string };

// โš ๏ธ Works, but string literals are cleaner and more explicit

// โœ… Better - use string literal types!
type CleanResponse = 
  | { status: 'loading'; progress: number }
  | { status: 'success'; data: string }
  | { status: 'error'; error: string };

// ๐ŸŽฏ Much cleaner to work with!
function handleResponse(response: CleanResponse) {
  switch (response.status) {
    case 'loading':
      return `Loading... ${response.progress}%`;
    case 'success':
      return `Data: ${response.data}`;
    case 'error':
      return `Error: ${response.error}`;
  }
}

๐Ÿ”ฅ Pitfall 3: Missing Exhaustiveness Checking

// โŒ Dangerous - might miss cases when adding new variants!
type Animal = 
  | { species: 'dog'; breed: string; good: true }
  | { species: 'cat'; indoor: boolean; sass: number }
  | { species: 'bird'; canFly: boolean; volume: number };

function describeAnimal(animal: Animal): string {
  switch (animal.species) {
    case 'dog':
      return `๐Ÿ• Good doggo! Breed: ${animal.breed}`;
    case 'cat':
      return `๐Ÿฑ Cat with sass level: ${animal.sass}`;
    // ๐Ÿ˜ฑ Forgot birds! No compile error!
  }
  // ๐Ÿ’ฅ Runtime error if we get a bird!
}

// โœ… Safe - use exhaustiveness checking!
function describeSafeAnimal(animal: Animal): string {
  switch (animal.species) {
    case 'dog':
      return `๐Ÿ• Good doggo! Breed: ${animal.breed}`;
    case 'cat':
      return `๐Ÿฑ Cat with sass level: ${animal.sass}`;
    case 'bird':
      return `๐Ÿฆ Bird volume: ${animal.volume}/10`;
    default:
      // ๐Ÿ›ก๏ธ This ensures we handle all cases!
      const exhaustiveCheck: never = animal;
      return exhaustiveCheck;
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Always Use a Discriminant: Include a common property with literal types
  2. ๐Ÿ“ Make Discriminants Meaningful: Use descriptive names like status, type, or kind
  3. ๐Ÿ›ก๏ธ Enable Exhaustiveness Checking: Use never type to catch missing cases
  4. ๐ŸŽจ Prefer String Literals: Use 'loading' instead of enums when possible
  5. โœจ Keep States Separate: Donโ€™t share properties between different states unless theyโ€™re truly common
  6. ๐Ÿ”’ Make Impossible States Impossible: Design your unions so invalid combinations canโ€™t exist
  7. ๐Ÿงช Add Helpers: Create utility functions for common operations like state transitions

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Media Player State Machine

Create a type-safe media player using discriminated unions:

๐Ÿ“‹ Requirements:

  • ๐ŸŽต Player states: idle, loading, playing, paused, error
  • ๐Ÿ“Š Each state should have appropriate data (current song, progress, error message, etc.)
  • ๐ŸŽฎ State transition validation
  • ๐Ÿ”„ Playlist management
  • ๐ŸŽจ Progress tracking and metadata!

๐Ÿš€ Bonus Points:

  • Add volume control states
  • Implement shuffle/repeat modes
  • Create a queue system with different priorities

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Media player state machine with discriminated unions!
type Song = {
  id: string;
  title: string;
  artist: string;
  duration: number;
  url: string;
};

type PlayerState = 
  | { status: 'idle'; lastPlayed?: Song }
  | { status: 'loading'; song: Song; progress: number }
  | { 
      status: 'playing'; 
      song: Song; 
      currentTime: number; 
      volume: number;
      playlistPosition: number;
    }
  | { 
      status: 'paused'; 
      song: Song; 
      pausedAt: number; 
      volume: number;
      playlistPosition: number;
    }
  | { 
      status: 'error'; 
      song?: Song; 
      error: string; 
      retryCount: number;
    };

class MediaPlayer {
  private state: PlayerState = { status: 'idle' };
  private playlist: Song[] = [];
  
  // ๐ŸŽต Get current state description
  getStateDescription(): string {
    switch (this.state.status) {
      case 'idle':
        const last = this.state.lastPlayed;
        return last ? `๐Ÿ’ค Idle (last: ${last.title})` : '๐Ÿ’ค Ready to play';
      
      case 'loading':
        return `โณ Loading ${this.state.song.title} (${this.state.progress}%)`;
      
      case 'playing':
        const progress = Math.round((this.state.currentTime / this.state.song.duration) * 100);
        return `๐ŸŽต Playing: ${this.state.song.title} by ${this.state.song.artist} (${progress}%)`;
      
      case 'paused':
        const pauseProgress = Math.round((this.state.pausedAt / this.state.song.duration) * 100);
        return `โธ๏ธ Paused: ${this.state.song.title} at ${pauseProgress}%`;
      
      case 'error':
        const songInfo = this.state.song ? ` (${this.state.song.title})` : '';
        return `โŒ Error${songInfo}: ${this.state.error}`;
    }
  }
  
  // ๐Ÿ”„ State transitions with validation
  canTransition(from: PlayerState['status'], to: PlayerState['status']): boolean {
    const transitions: Record<PlayerState['status'], PlayerState['status'][]> = {
      idle: ['loading'],
      loading: ['playing', 'error', 'idle'],
      playing: ['paused', 'loading', 'error', 'idle'],
      paused: ['playing', 'loading', 'error', 'idle'],
      error: ['loading', 'idle']
    };
    
    return transitions[from].includes(to);
  }
  
  // โ–ถ๏ธ Play a song
  play(song: Song): void {
    if (!this.canTransition(this.state.status, 'loading')) {
      console.log(`โŒ Cannot start loading from ${this.state.status} state`);
      return;
    }
    
    // Start loading
    this.state = { status: 'loading', song, progress: 0 };
    
    // Simulate loading
    setTimeout(() => {
      this.state = {
        status: 'playing',
        song,
        currentTime: 0,
        volume: 0.8,
        playlistPosition: this.playlist.findIndex(s => s.id === song.id)
      };
      console.log(`๐ŸŽต Now playing: ${song.title}`);
    }, 1000);
  }
  
  // โธ๏ธ Pause playback
  pause(): void {
    if (this.state.status === 'playing') {
      this.state = {
        status: 'paused',
        song: this.state.song,
        pausedAt: this.state.currentTime,
        volume: this.state.volume,
        playlistPosition: this.state.playlistPosition
      };
      console.log(`โธ๏ธ Paused at ${Math.round(this.state.pausedAt)}s`);
    }
  }
  
  // โ–ถ๏ธ Resume playback
  resume(): void {
    if (this.state.status === 'paused') {
      this.state = {
        status: 'playing',
        song: this.state.song,
        currentTime: this.state.pausedAt,
        volume: this.state.volume,
        playlistPosition: this.state.playlistPosition
      };
      console.log(`โ–ถ๏ธ Resumed ${this.state.song.title}`);
    }
  }
}

// ๐ŸŽฎ Test the media player!
const player = new MediaPlayer();
const song: Song = {
  id: '1',
  title: 'TypeScript Symphony',
  artist: 'The Compiler Band',
  duration: 240,
  url: 'music/typescript-symphony.mp3'
};

console.log(player.getStateDescription()); // ๐Ÿ’ค Ready to play
player.play(song);
console.log(player.getStateDescription()); // โณ Loading...

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered one of TypeScriptโ€™s most powerful features! Hereโ€™s what you can now do:

  • โœ… Create bulletproof discriminated unions with perfect type safety ๐Ÿ’ช
  • โœ… Implement exhaustive pattern matching that catches all edge cases ๐Ÿ›ก๏ธ
  • โœ… Build type-safe state machines for complex workflows ๐ŸŽฏ
  • โœ… Eliminate impossible states from your applications ๐Ÿ›
  • โœ… Design APIs that are impossible to misuse ๐Ÿš€

Remember: Discriminated unions turn runtime errors into compile-time guarantees. Theyโ€™re your secret weapon for building rock-solid applications! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered discriminated unions and pattern matching!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the media player exercise above
  2. ๐Ÿ—๏ธ Refactor existing code to use discriminated unions instead of optional properties
  3. ๐Ÿ“š Move on to our next tutorial: Exhaustiveness Checking: Complete Pattern Coverage
  4. ๐ŸŒŸ Share your type-safe creations with the community!

Remember: Every impossible state you eliminate makes your application more reliable. Keep building bulletproof software! ๐Ÿš€


Happy pattern matching! ๐ŸŽ‰๐ŸŽฏโœจ