+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 13 of 355

๐Ÿ“š Arrays and Tuples in TypeScript: Working with Collections Like a Pro

Master TypeScript arrays and tuples to handle collections of data with confidence, type safety, and real-world best practices ๐Ÿš€

๐ŸŒฑBeginner
20 min read

Prerequisites

  • Basic TypeScript types knowledge ๐Ÿ“
  • Understanding of JavaScript arrays โšก
  • TypeScript development environment ๐Ÿ’ป

What you'll learn

  • Master array types and operations ๐ŸŽฏ
  • Understand tuples for fixed-length collections ๐Ÿ—๏ธ
  • Apply array methods with type safety ๐Ÿ”
  • Build real-world collection handling โœจ

๐ŸŽฏ Introduction

Welcome to the wonderful world of TypeScript arrays and tuples! ๐ŸŽ‰ In this guide, weโ€™ll explore how to work with collections of data in a type-safe way that makes your code bulletproof.

Think of arrays as your trusty toolbox ๐Ÿงฐ - they hold multiple items of the same type. Tuples are like a perfectly organized drawer ๐Ÿ—‚๏ธ - each slot has a specific purpose and type. Together, theyโ€™re your dynamic duo for handling collections in TypeScript!

By the end of this tutorial, youโ€™ll be confidently managing lists, coordinates, shopping carts, and more! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Arrays and Tuples

๐Ÿค” What Are Arrays in TypeScript?

Arrays in TypeScript are like supercharged JavaScript arrays ๐Ÿš€. Theyโ€™re ordered collections that can grow or shrink, but with one crucial difference - TypeScript ensures all elements are of the same type!

In TypeScript, arrays give you:

  • โœจ Type safety for all elements
  • ๐Ÿš€ Better autocomplete and IntelliSense
  • ๐Ÿ›ก๏ธ Protection from type-related bugs
  • ๐Ÿ“– Self-documenting code

๐Ÿ’ก What Are Tuples?

Tuples are TypeScriptโ€™s special arrays with a twist ๐ŸŒช๏ธ. They have:

  • ๐Ÿ“ Fixed length
  • ๐ŸŽฏ Specific type for each position
  • ๐Ÿ”’ Order matters!

Think of tuples like coordinates on a map ๐Ÿ—บ๏ธ - you need exactly two numbers (latitude, longitude) in that specific order!

๐Ÿ”ง Working with Arrays

๐Ÿ“ Array Basics

Letโ€™s start with the fundamentals:

// ๐ŸŽจ Two ways to declare arrays
let numbers: number[] = [1, 2, 3, 4, 5];
let fruits: Array<string> = ["apple", "banana", "orange"];

// ๐Ÿš€ TypeScript infers the type
let scores = [95, 87, 92]; // TypeScript knows it's number[]

// โœจ Mixed arrays need union types
let mixed: (string | number)[] = ["hello", 42, "world", 100];

// ๐ŸŽฏ Array of objects
interface Player {
  name: string;
  score: number;
  emoji: string;
}

let leaderboard: Player[] = [
  { name: "Alice", score: 9500, emoji: "๐Ÿฅ‡" },
  { name: "Bob", score: 8700, emoji: "๐Ÿฅˆ" },
  { name: "Charlie", score: 8200, emoji: "๐Ÿฅ‰" }
];

๐ŸŽฎ Array Methods with Type Safety

TypeScript makes array methods safer and smarter:

// ๐Ÿ›’ Shopping list example
interface Item {
  id: number;
  name: string;
  price: number;
  emoji: string;
  quantity: number;
}

let shoppingCart: Item[] = [
  { id: 1, name: "TypeScript Book", price: 39.99, emoji: "๐Ÿ“˜", quantity: 1 },
  { id: 2, name: "Coffee", price: 4.99, emoji: "โ˜•", quantity: 3 },
  { id: 3, name: "Mechanical Keyboard", price: 149.99, emoji: "โŒจ๏ธ", quantity: 1 }
];

// ๐ŸŽฏ Map - transform each element
const itemNames: string[] = shoppingCart.map(item => 
  `${item.emoji} ${item.name}`
);
console.log("Items:", itemNames);
// Output: ["๐Ÿ“˜ TypeScript Book", "โ˜• Coffee", "โŒจ๏ธ Mechanical Keyboard"]

// ๐Ÿ’ฐ Reduce - calculate total
const total: number = shoppingCart.reduce((sum, item) => 
  sum + (item.price * item.quantity), 0
);
console.log(`Total: $${total.toFixed(2)} ๐Ÿ’ธ`);

// ๐Ÿ” Filter - find expensive items
const expensiveItems: Item[] = shoppingCart.filter(item => 
  item.price > 50
);
console.log("Expensive items:", expensiveItems.map(i => i.emoji));

// โœจ Find - get first match
const coffeeItem: Item | undefined = shoppingCart.find(item => 
  item.name.includes("Coffee")
);
if (coffeeItem) {
  console.log(`Found ${coffeeItem.emoji} at $${coffeeItem.price}`);
}

// ๐ŸŽจ Some and Every
const hasExpensiveItem: boolean = shoppingCart.some(item => item.price > 100);
const allUnderBudget: boolean = shoppingCart.every(item => item.price < 200);

๐Ÿ—๏ธ Multi-dimensional Arrays

Arrays can contain other arrays:

// ๐ŸŽฎ Game board (Tic-Tac-Toe)
type Cell = "X" | "O" | " ";
type Board = Cell[][];

let gameBoard: Board = [
  ["X", "O", "X"],
  ["O", "X", "O"],
  [" ", " ", "X"]
];

// ๐Ÿ—บ๏ธ Map coordinates
type Coordinate = [number, number];
let treasureMap: Coordinate[] = [
  [10, 20],
  [30, 40],
  [50, 60]
];

// ๐Ÿ“Š Data matrix
let salesData: number[][] = [
  [100, 150, 200], // Q1
  [120, 180, 220], // Q2
  [140, 200, 250], // Q3
  [160, 220, 280]  // Q4
];

// ๐ŸŽฏ Working with 2D arrays
function printBoard(board: Board): void {
  console.log("๐ŸŽฎ Game Board:");
  board.forEach((row, i) => {
    console.log(`  ${row.map(cell => cell || "ยท").join(" | ")}`);
    if (i < board.length - 1) console.log("  ---------");
  });
}

๐Ÿ’ก Understanding Tuples

๐ŸŽฏ Tuple Basics

Tuples are perfect for fixed-size, ordered data:

// ๐Ÿ“ Simple tuple - coordinates
let position: [number, number] = [10, 20];
let rgb: [number, number, number] = [255, 128, 0];

// ๐Ÿท๏ธ Named tuples (TypeScript 4.0+)
type Point3D = [x: number, y: number, z: number];
let playerPosition: Point3D = [100, 50, 25];

// ๐ŸŽจ Mixed type tuples
type UserRecord = [id: number, name: string, isActive: boolean];
let user: UserRecord = [1, "Alice", true];

// ๐Ÿ“Š Database result tuple
type QueryResult = [data: any[], count: number, hasMore: boolean];
let result: QueryResult = [
  [{id: 1, name: "Item 1"}, {id: 2, name: "Item 2"}],
  2,
  false
];

// โœจ Destructuring tuples
let [x, y, z] = playerPosition;
console.log(`Player at: X=${x}, Y=${y}, Z=${z}`);

// ๐ŸŽฎ Function returning tuple
function getPlayerStats(): [health: number, mana: number, level: number] {
  return [100, 50, 12];
}

let [health, mana, level] = getPlayerStats();
console.log(`โšก Health: ${health} | ๐Ÿ’™ Mana: ${mana} | ๐ŸŽฏ Level: ${level}`);

๐Ÿš€ Advanced Tuple Patterns

Tuples shine in specific scenarios:

// ๐ŸŒˆ Color system with alpha
type RGBA = [red: number, green: number, blue: number, alpha: number];

class ColorPalette {
  private colors: Map<string, RGBA> = new Map();
  
  // ๐ŸŽจ Add color
  addColor(name: string, color: RGBA): void {
    this.colors.set(name, color);
    console.log(`๐ŸŽจ Added color "${name}"`);
  }
  
  // ๐Ÿ”„ Mix two colors
  mixColors(color1: RGBA, color2: RGBA): RGBA {
    return [
      Math.round((color1[0] + color2[0]) / 2),
      Math.round((color1[1] + color2[1]) / 2),
      Math.round((color1[2] + color2[2]) / 2),
      (color1[3] + color2[3]) / 2
    ];
  }
  
  // ๐Ÿ“ Convert to CSS
  toCSS(color: RGBA): string {
    return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;
  }
}

// ๐ŸŽฎ Game inventory slots
type InventorySlot = [
  item: string | null,
  quantity: number,
  maxStack: number
];

class Inventory {
  private slots: InventorySlot[] = [];
  
  constructor(size: number) {
    // Initialize empty slots
    for (let i = 0; i < size; i++) {
      this.slots.push([null, 0, 99]);
    }
  }
  
  // ๐ŸŽ’ Add item to inventory
  addItem(itemName: string, quantity: number = 1): boolean {
    // Find empty or matching slot
    for (let i = 0; i < this.slots.length; i++) {
      let [item, qty, max] = this.slots[i];
      
      if (item === null || (item === itemName && qty < max)) {
        const spaceAvailable = item === null ? max : max - qty;
        const toAdd = Math.min(quantity, spaceAvailable);
        
        this.slots[i] = [
          itemName,
          (item === null ? 0 : qty) + toAdd,
          max
        ];
        
        console.log(`โœ… Added ${toAdd}x ${itemName} to slot ${i}`);
        return toAdd === quantity;
      }
    }
    
    console.log(`โŒ No space for ${itemName}!`);
    return false;
  }
}

๐Ÿš€ Practical Examples

๐Ÿ“Š Data Analysis Dashboard

Letโ€™s build a real data analysis system:

// ๐Ÿ“ˆ Stock market data
type StockPrice = [timestamp: Date, open: number, high: number, low: number, close: number];
type StockSymbol = string;

class StockTracker {
  private stocks: Map<StockSymbol, StockPrice[]> = new Map();
  
  // ๐Ÿ“Š Add price data
  addPriceData(symbol: StockSymbol, price: StockPrice): void {
    if (!this.stocks.has(symbol)) {
      this.stocks.set(symbol, []);
    }
    this.stocks.get(symbol)!.push(price);
    
    const [time, open, high, low, close] = price;
    console.log(`๐Ÿ“ˆ ${symbol}: $${close} at ${time.toLocaleTimeString()}`);
  }
  
  // ๐Ÿ’น Calculate moving average
  getMovingAverage(symbol: StockSymbol, periods: number): number | null {
    const prices = this.stocks.get(symbol);
    if (!prices || prices.length < periods) return null;
    
    const recentPrices = prices.slice(-periods);
    const sum = recentPrices.reduce((total, [, , , , close]) => total + close, 0);
    
    return sum / periods;
  }
  
  // ๐ŸŽฏ Find highest price
  getHighestPrice(symbol: StockSymbol): number | null {
    const prices = this.stocks.get(symbol);
    if (!prices || prices.length === 0) return null;
    
    return Math.max(...prices.map(([, , high]) => high));
  }
}

// ๐ŸŽฎ Game leaderboard system
interface Score {
  player: string;
  points: number;
  level: number;
  achievements: string[];
  timestamp: Date;
}

class Leaderboard {
  private scores: Score[] = [];
  private readonly MAX_ENTRIES = 10;
  
  // ๐Ÿ† Add score
  addScore(score: Score): void {
    this.scores.push(score);
    this.scores.sort((a, b) => b.points - a.points);
    this.scores = this.scores.slice(0, this.MAX_ENTRIES);
    
    console.log(`๐ŸŽฏ New score: ${score.player} - ${score.points} points!`);
    this.displayTop3();
  }
  
  // ๐Ÿฅ‡ Display top 3
  private displayTop3(): void {
    console.log("\n๐Ÿ† Top 3 Players:");
    const medals = ["๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰"];
    
    this.scores.slice(0, 3).forEach((score, index) => {
      console.log(`${medals[index]} ${score.player}: ${score.points} pts (Level ${score.level})`);
    });
  }
  
  // ๐Ÿ“Š Get statistics
  getStats(): [average: number, highest: number, totalGames: number] {
    if (this.scores.length === 0) return [0, 0, 0];
    
    const total = this.scores.reduce((sum, s) => sum + s.points, 0);
    const average = total / this.scores.length;
    const highest = this.scores[0].points;
    
    return [average, highest, this.scores.length];
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Array Mutation Confusion

// โŒ Dangerous - mutating arrays unexpectedly
const originalArray = [1, 2, 3];
const copiedArray = originalArray; // This is NOT a copy!
copiedArray.push(4);
console.log(originalArray); // [1, 2, 3, 4] ๐Ÿ˜ฑ

// โœ… Safe - proper array copying
const properCopy1 = [...originalArray]; // Spread operator
const properCopy2 = originalArray.slice(); // Slice method
const properCopy3 = Array.from(originalArray); // Array.from

// ๐ŸŽฏ Deep copying for nested arrays
const nestedArray = [[1, 2], [3, 4]];
const deepCopy = nestedArray.map(arr => [...arr]);

๐Ÿคฏ Pitfall 2: Tuple vs Array Confusion

// โŒ Wrong - treating tuple as array
let coordinate: [number, number] = [10, 20];
// coordinate.push(30); // Error! Tuples have fixed length

// โŒ Wrong - wrong types in tuple positions
// let user: [string, number] = [25, "Alice"]; // Error! Wrong order

// โœ… Correct - respecting tuple structure
let user: [name: string, age: number] = ["Alice", 25];
let [userName, userAge] = user; // Destructuring works great!

// ๐Ÿ’ก Convert tuple to array when needed
let flexibleArray: number[] = [...coordinate];
flexibleArray.push(30); // Now this is OK!

๐Ÿ˜ต Pitfall 3: Empty Array Type Inference

// โŒ Problematic - TypeScript can't infer the type
const items = []; // Type is never[]
// items.push("hello"); // Error!

// โœ… Solution 1 - Type annotation
const items1: string[] = [];
items1.push("hello"); // Works!

// โœ… Solution 2 - Type assertion
const items2 = [] as string[];

// โœ… Solution 3 - Generic array constructor
const items3 = new Array<string>();

๐Ÿ› ๏ธ Best Practices

๐ŸŽฏ Array Best Practices

  1. ๐Ÿ”’ Prefer Readonly Arrays: When arrays shouldnโ€™t change

    function processScores(scores: readonly number[]): number {
      // scores.push(100); // Error! Can't modify
      return scores.reduce((a, b) => a + b, 0);
    }
  2. ๐ŸŽจ Use Descriptive Types: Make intent clear

    type ProductID = string;
    type ShoppingCart = ProductID[];
    // Better than just string[]
  3. โœจ Leverage Array Methods: Donโ€™t reinvent the wheel

    // โœ… Good - using built-in methods
    const doubled = numbers.map(n => n * 2);
    
    // โŒ Avoid - manual loops when not needed
    const doubled2 = [];
    for (let i = 0; i < numbers.length; i++) {
      doubled2.push(numbers[i] * 2);
    }
  4. ๐Ÿ›ก๏ธ Guard Against Empty Arrays: Handle edge cases

    function getAverage(numbers: number[]): number {
      if (numbers.length === 0) return 0; // Guard clause
      return numbers.reduce((a, b) => a + b) / numbers.length;
    }
  5. ๐Ÿš€ Use Type Guards: For mixed arrays

    function processItems(items: (string | number)[]) {
      items.forEach(item => {
        if (typeof item === 'string') {
          console.log(`Text: ${item.toUpperCase()}`);
        } else {
          console.log(`Number: ${item * 2}`);
        }
      });
    }

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Task Management System

Create a type-safe task tracker with arrays and tuples:

๐Ÿ“‹ Requirements:

  • โœ… Store tasks in typed arrays
  • ๐Ÿท๏ธ Use tuples for task metadata [created: Date, updated: Date]
  • ๐Ÿ‘ฅ Track assigned users with arrays
  • ๐Ÿ“… Implement task filtering and sorting
  • ๐ŸŽจ Add emoji status indicators

๐Ÿš€ Bonus Points:

  • Implement task history using tuples
  • Add batch operations on task arrays
  • Create statistics with array reduction

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Task management system with arrays and tuples
type TaskStatus = "todo" | "in-progress" | "done" | "archived";
type TaskPriority = "low" | "medium" | "high";
type TaskMetadata = [created: Date, updated: Date, version: number];

interface Task {
  id: string;
  title: string;
  description: string;
  status: TaskStatus;
  priority: TaskPriority;
  assignees: string[];
  tags: string[];
  metadata: TaskMetadata;
  emoji: string;
}

class TaskManager {
  private tasks: Task[] = [];
  private history: Array<[taskId: string, action: string, timestamp: Date]> = [];
  
  // โž• Create new task
  createTask(
    title: string,
    description: string,
    priority: TaskPriority = "medium"
  ): Task {
    const now = new Date();
    const task: Task = {
      id: `TASK-${Date.now()}`,
      title,
      description,
      status: "todo",
      priority,
      assignees: [],
      tags: [],
      metadata: [now, now, 1],
      emoji: this.getEmojiForPriority(priority)
    };
    
    this.tasks.push(task);
    this.addHistory(task.id, "created");
    console.log(`โœ… Created task: ${task.emoji} ${task.title}`);
    
    return task;
  }
  
  // ๐Ÿ‘ฅ Assign users to task
  assignUsers(taskId: string, users: string[]): void {
    const task = this.tasks.find(t => t.id === taskId);
    if (task) {
      task.assignees = [...new Set([...task.assignees, ...users])];
      this.updateTaskMetadata(task);
      this.addHistory(taskId, `assigned ${users.join(", ")}`);
      console.log(`๐Ÿ‘ฅ Assigned ${users.length} users to task`);
    }
  }
  
  // ๐Ÿ”„ Update task status
  updateStatus(taskId: string, status: TaskStatus): void {
    const task = this.tasks.find(t => t.id === taskId);
    if (task) {
      task.status = status;
      task.emoji = this.getEmojiForStatus(status);
      this.updateTaskMetadata(task);
      this.addHistory(taskId, `status changed to ${status}`);
      console.log(`๐Ÿ“ Task status updated to: ${status}`);
    }
  }
  
  // ๐Ÿ” Filter tasks
  filterTasks(
    criteria: {
      status?: TaskStatus;
      priority?: TaskPriority;
      assignee?: string;
      tag?: string;
    }
  ): Task[] {
    return this.tasks.filter(task => {
      if (criteria.status && task.status !== criteria.status) return false;
      if (criteria.priority && task.priority !== criteria.priority) return false;
      if (criteria.assignee && !task.assignees.includes(criteria.assignee)) return false;
      if (criteria.tag && !task.tags.includes(criteria.tag)) return false;
      return true;
    });
  }
  
  // ๐Ÿ“Š Get statistics
  getStats(): [total: number, byStatus: Record<TaskStatus, number>, avgAssignees: number] {
    const total = this.tasks.length;
    
    const byStatus = this.tasks.reduce((acc, task) => {
      acc[task.status] = (acc[task.status] || 0) + 1;
      return acc;
    }, {} as Record<TaskStatus, number>);
    
    const totalAssignees = this.tasks.reduce((sum, task) => 
      sum + task.assignees.length, 0
    );
    const avgAssignees = total > 0 ? totalAssignees / total : 0;
    
    return [total, byStatus, avgAssignees];
  }
  
  // ๐ŸŽฏ Batch operations
  batchUpdatePriority(taskIds: string[], priority: TaskPriority): void {
    const updatedTasks = this.tasks
      .filter(task => taskIds.includes(task.id))
      .map(task => {
        task.priority = priority;
        task.emoji = this.getEmojiForPriority(priority);
        this.updateTaskMetadata(task);
        return task;
      });
    
    console.log(`๐ŸŽฏ Updated priority for ${updatedTasks.length} tasks`);
  }
  
  // ๐Ÿ“ˆ Get task timeline
  getTaskTimeline(taskId: string): Array<[action: string, when: Date]> {
    return this.history
      .filter(([id]) => id === taskId)
      .map(([, action, timestamp]) => [action, timestamp]);
  }
  
  // Private helpers
  private updateTaskMetadata(task: Task): void {
    const [created, , version] = task.metadata;
    task.metadata = [created, new Date(), version + 1];
  }
  
  private addHistory(taskId: string, action: string): void {
    this.history.push([taskId, action, new Date()]);
  }
  
  private getEmojiForPriority(priority: TaskPriority): string {
    const emojis: Record<TaskPriority, string> = {
      low: "๐ŸŸข",
      medium: "๐ŸŸก",
      high: "๐Ÿ”ด"
    };
    return emojis[priority];
  }
  
  private getEmojiForStatus(status: TaskStatus): string {
    const emojis: Record<TaskStatus, string> = {
      todo: "๐Ÿ“‹",
      "in-progress": "๐Ÿšง",
      done: "โœ…",
      archived: "๐Ÿ“ฆ"
    };
    return emojis[status];
  }
  
  // ๐Ÿ“‹ Display tasks
  displayTasks(): void {
    console.log("\n๐Ÿ“‹ All Tasks:");
    this.tasks.forEach(task => {
      console.log(`${task.emoji} ${task.title} (${task.status})`);
      console.log(`   Priority: ${task.priority} | Assignees: ${task.assignees.join(", ") || "none"}`);
    });
    
    const [total, byStatus, avgAssignees] = this.getStats();
    console.log("\n๐Ÿ“Š Statistics:");
    console.log(`   Total tasks: ${total}`);
    console.log(`   By status:`, byStatus);
    console.log(`   Avg assignees: ${avgAssignees.toFixed(1)}`);
  }
}

// ๐ŸŽฎ Test the system
const taskManager = new TaskManager();

// Create tasks
const task1 = taskManager.createTask(
  "Learn TypeScript Arrays",
  "Master arrays and their methods",
  "high"
);

const task2 = taskManager.createTask(
  "Practice Tuples",
  "Understand fixed-length collections",
  "medium"
);

// Assign users
taskManager.assignUsers(task1.id, ["Alice", "Bob"]);
taskManager.assignUsers(task2.id, ["Charlie"]);

// Update status
taskManager.updateStatus(task1.id, "in-progress");

// Filter tasks
const highPriorityTasks = taskManager.filterTasks({ priority: "high" });
console.log(`\n๐Ÿ”ฅ High priority tasks: ${highPriorityTasks.length}`);

// Display all
taskManager.displayTasks();

// Check timeline
const timeline = taskManager.getTaskTimeline(task1.id);
console.log("\n๐Ÿ“… Task Timeline:");
timeline.forEach(([action, when]) => {
  console.log(`   ${when.toLocaleTimeString()}: ${action}`);
});

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered arrays and tuples in TypeScript! Hereโ€™s what you can now do:

  • โœ… Create type-safe arrays for any data collection ๐Ÿ’ช
  • โœ… Use array methods with full type inference ๐ŸŽฏ
  • โœ… Work with tuples for fixed-size data structures ๐Ÿ—๏ธ
  • โœ… Handle multi-dimensional arrays like a pro ๐Ÿ”
  • โœ… Build real-world applications with confidence! ๐Ÿš€

Remember: Arrays and tuples are your foundation for handling collections in TypeScript. Use arrays for dynamic lists and tuples for fixed structures! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve become an arrays and tuples expert!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Complete the task management exercise
  2. ๐Ÿ—๏ธ Refactor existing code to use proper array types
  3. ๐Ÿ“š Learn about object types and type aliases
  4. ๐ŸŒŸ Explore advanced array manipulation patterns!

Remember: Every shopping cart, game leaderboard, and data dashboard starts with arrays. Keep practicing! ๐Ÿš€

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