+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 58 of 72

๐Ÿ“ฆ Generic Classes: Flexible Data Structures

Master generic classes in TypeScript to build reusable data structures and containers that work with any type ๐Ÿš€

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Understanding of TypeScript generics basics ๐Ÿ“
  • Knowledge of classes and OOP concepts ๐Ÿ”
  • Familiarity with data structures ๐Ÿ’ป

What you'll learn

  • Create powerful generic data structures ๐ŸŽฏ
  • Build type-safe containers and collections ๐Ÿ—๏ธ
  • Implement complex generic class patterns ๐Ÿ›ก๏ธ
  • Design reusable class-based libraries โœจ

๐ŸŽฏ Introduction

Welcome to the world of generic classes! ๐ŸŽ‰ In this guide, weโ€™ll explore how to create flexible, reusable classes that can work with any type while maintaining complete type safety.

Youโ€™ll discover how generic classes are like universal containers ๐Ÿ“ฆ - they provide structure and behavior that adapts to whatever you put inside! Whether youโ€™re building collections ๐Ÿ“š, data structures ๐ŸŒณ, or complex state management systems ๐ŸŽฎ, mastering generic classes is essential for creating professional TypeScript libraries.

By the end of this tutorial, youโ€™ll be confidently building generic classes that solve real-world problems elegantly! Letโ€™s create some powerful data structures! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Generic Classes

๐Ÿค” Why Generic Classes?

Generic classes allow you to create reusable components that maintain type information throughout their lifecycle:

// โŒ Without generics - lose type safety
class Container {
  private value: any;
  
  constructor(value: any) {
    this.value = value;
  }
  
  getValue(): any {
    return this.value;
  }
}

const numberContainer = new Container(42);
const value = numberContainer.getValue(); // Type is 'any' ๐Ÿ˜ข

// โœ… With generics - maintain type safety
class GenericContainer<T> {
  private value: T;
  
  constructor(value: T) {
    this.value = value;
  }
  
  getValue(): T {
    return this.value;
  }
  
  setValue(value: T): void {
    this.value = value;
  }
}

const numberContainer2 = new GenericContainer(42);
const value2 = numberContainer2.getValue(); // Type is 'number' โœจ
console.log(value2.toFixed(2)); // IntelliSense works!

const stringContainer = new GenericContainer('hello');
console.log(stringContainer.getValue().toUpperCase()); // String methods available!

๐Ÿ’ก Generic Class Syntax

Understanding the anatomy of generic classes:

// ๐ŸŽฏ Basic generic class structure
class ClassName<T> {
  //           ^^^ Type parameter declaration
  
  private property: T;
  //                ^ Using type parameter
  
  constructor(value: T) {
    //               ^ Type parameter in constructor
    this.property = value;
  }
  
  method(): T {
    //      ^ Return type using parameter
    return this.property;
  }
}

// ๐Ÿ—๏ธ Multiple type parameters
class Pair<T, U> {
  constructor(
    public first: T,
    public second: U
  ) {}
  
  swap(): Pair<U, T> {
    return new Pair(this.second, this.first);
  }
}

const pair = new Pair('hello', 42);
const swapped = pair.swap(); // Pair<number, string>

// ๐Ÿ”ง Static members in generic classes
class Registry<T> {
  private static instances = new Map<string, Registry<any>>();
  private items = new Map<string, T>();
  
  constructor(private name: string) {
    Registry.instances.set(name, this);
  }
  
  static getInstance<U>(name: string): Registry<U> | undefined {
    return Registry.instances.get(name);
  }
  
  add(key: string, item: T): void {
    this.items.set(key, item);
  }
  
  get(key: string): T | undefined {
    return this.items.get(key);
  }
}

๐Ÿš€ Building Data Structures

๐Ÿ“Š Stack Implementation

Creating a type-safe stack data structure:

// ๐ŸŽฏ Generic Stack class
class Stack<T> {
  private items: T[] = [];
  private readonly maxSize?: number;
  
  constructor(maxSize?: number) {
    this.maxSize = maxSize;
  }
  
  push(item: T): boolean {
    if (this.maxSize && this.items.length >= this.maxSize) {
      return false; // Stack overflow
    }
    this.items.push(item);
    return true;
  }
  
  pop(): T | undefined {
    return this.items.pop();
  }
  
  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
  
  isEmpty(): boolean {
    return this.items.length === 0;
  }
  
  isFull(): boolean {
    return this.maxSize !== undefined && this.items.length >= this.maxSize;
  }
  
  size(): number {
    return this.items.length;
  }
  
  clear(): void {
    this.items = [];
  }
  
  toArray(): T[] {
    return [...this.items];
  }
  
  // Iterator support
  *[Symbol.iterator](): Iterator<T> {
    for (let i = this.items.length - 1; i >= 0; i--) {
      yield this.items[i];
    }
  }
}

// ๐Ÿ’ซ Usage
const numberStack = new Stack<number>(5);
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);

console.log(numberStack.peek()); // 3
console.log(numberStack.pop()); // 3
console.log(numberStack.size()); // 2

// Iterate over stack
for (const num of numberStack) {
  console.log(num); // 2, 1 (LIFO order)
}

// Complex type stack
interface Task {
  id: string;
  priority: number;
  description: string;
}

const taskStack = new Stack<Task>();
taskStack.push({ id: '1', priority: 1, description: 'Low priority' });
taskStack.push({ id: '2', priority: 5, description: 'High priority' });

๐ŸŒณ Binary Tree Implementation

Building a generic binary search tree:

// ๐ŸŽฏ Tree node class
class TreeNode<T> {
  constructor(
    public value: T,
    public left: TreeNode<T> | null = null,
    public right: TreeNode<T> | null = null
  ) {}
}

// ๐Ÿ—๏ธ Binary Search Tree with constraints
class BinarySearchTree<T> {
  private root: TreeNode<T> | null = null;
  
  constructor(
    private compareFn: (a: T, b: T) => number
  ) {}
  
  insert(value: T): void {
    this.root = this.insertNode(this.root, value);
  }
  
  private insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> {
    if (!node) {
      return new TreeNode(value);
    }
    
    const comparison = this.compareFn(value, node.value);
    
    if (comparison < 0) {
      node.left = this.insertNode(node.left, value);
    } else if (comparison > 0) {
      node.right = this.insertNode(node.right, value);
    }
    
    return node;
  }
  
  search(value: T): boolean {
    return this.searchNode(this.root, value);
  }
  
  private searchNode(node: TreeNode<T> | null, value: T): boolean {
    if (!node) return false;
    
    const comparison = this.compareFn(value, node.value);
    
    if (comparison === 0) return true;
    if (comparison < 0) return this.searchNode(node.left, value);
    return this.searchNode(node.right, value);
  }
  
  // In-order traversal
  *inOrder(): Generator<T> {
    yield* this.inOrderTraversal(this.root);
  }
  
  private *inOrderTraversal(node: TreeNode<T> | null): Generator<T> {
    if (!node) return;
    
    yield* this.inOrderTraversal(node.left);
    yield node.value;
    yield* this.inOrderTraversal(node.right);
  }
  
  // Convert to sorted array
  toArray(): T[] {
    return [...this.inOrder()];
  }
  
  // Get min and max values
  min(): T | undefined {
    if (!this.root) return undefined;
    
    let current = this.root;
    while (current.left) {
      current = current.left;
    }
    return current.value;
  }
  
  max(): T | undefined {
    if (!this.root) return undefined;
    
    let current = this.root;
    while (current.right) {
      current = current.right;
    }
    return current.value;
  }
}

// ๐Ÿ’ซ Usage with different types
// Number tree
const numberTree = new BinarySearchTree<number>((a, b) => a - b);
[5, 3, 7, 1, 9, 4, 6].forEach(n => numberTree.insert(n));

console.log(numberTree.search(4)); // true
console.log(numberTree.toArray()); // [1, 3, 4, 5, 6, 7, 9]
console.log(numberTree.min(), numberTree.max()); // 1, 9

// String tree
const stringTree = new BinarySearchTree<string>((a, b) => a.localeCompare(b));
['dog', 'cat', 'elephant', 'ant', 'bear'].forEach(s => stringTree.insert(s));

console.log(stringTree.toArray()); // ['ant', 'bear', 'cat', 'dog', 'elephant']

// Custom object tree
interface Person {
  name: string;
  age: number;
}

const personTree = new BinarySearchTree<Person>((a, b) => a.age - b.age);
personTree.insert({ name: 'Alice', age: 30 });
personTree.insert({ name: 'Bob', age: 25 });
personTree.insert({ name: 'Charlie', age: 35 });

for (const person of personTree.inOrder()) {
  console.log(`${person.name}: ${person.age}`);
}

๐ŸŽจ Advanced Generic Patterns

๐Ÿ”„ Observable Collections

Building reactive data structures:

// ๐ŸŽฏ Observable class with generics
type Observer<T> = (value: T, oldValue?: T) => void;

class ObservableValue<T> {
  private observers = new Set<Observer<T>>();
  private _value: T;
  
  constructor(initialValue: T) {
    this._value = initialValue;
  }
  
  get value(): T {
    return this._value;
  }
  
  set value(newValue: T) {
    const oldValue = this._value;
    this._value = newValue;
    this.notify(newValue, oldValue);
  }
  
  subscribe(observer: Observer<T>): () => void {
    this.observers.add(observer);
    return () => this.observers.delete(observer);
  }
  
  private notify(value: T, oldValue?: T): void {
    this.observers.forEach(observer => observer(value, oldValue));
  }
}

// ๐Ÿ—๏ธ Observable collection
class ObservableList<T> {
  private items: T[] = [];
  private observers = new Map<string, Set<Function>>();
  
  push(...items: T[]): number {
    const length = this.items.push(...items);
    this.emit('add', items);
    this.emit('change', this.items);
    return length;
  }
  
  pop(): T | undefined {
    const item = this.items.pop();
    if (item !== undefined) {
      this.emit('remove', [item]);
      this.emit('change', this.items);
    }
    return item;
  }
  
  get(index: number): T | undefined {
    return this.items[index];
  }
  
  set(index: number, value: T): void {
    const oldValue = this.items[index];
    this.items[index] = value;
    this.emit('update', { index, oldValue, newValue: value });
    this.emit('change', this.items);
  }
  
  on(event: string, callback: Function): () => void {
    if (!this.observers.has(event)) {
      this.observers.set(event, new Set());
    }
    this.observers.get(event)!.add(callback);
    
    return () => this.observers.get(event)?.delete(callback);
  }
  
  private emit(event: string, data: any): void {
    this.observers.get(event)?.forEach(callback => callback(data));
  }
  
  toArray(): T[] {
    return [...this.items];
  }
  
  get length(): number {
    return this.items.length;
  }
}

// ๐Ÿ’ซ Usage
const numbers = new ObservableList<number>();

numbers.on('add', (items: number[]) => {
  console.log('Added:', items);
});

numbers.on('change', (allItems: number[]) => {
  console.log('Current list:', allItems);
});

numbers.push(1, 2, 3); // Triggers both events
numbers.set(1, 20); // Updates index 1

๐Ÿ—๏ธ Generic Builder Pattern

Creating flexible builders with generics:

// ๐ŸŽฏ Generic builder class
class Builder<T> {
  private object: Partial<T> = {};
  private validators = new Map<keyof T, (value: any) => boolean>();
  
  set<K extends keyof T>(key: K, value: T[K]): this {
    const validator = this.validators.get(key);
    if (validator && !validator(value)) {
      throw new Error(`Invalid value for ${String(key)}`);
    }
    
    this.object[key] = value;
    return this;
  }
  
  setMany(values: Partial<T>): this {
    Object.entries(values).forEach(([key, value]) => {
      this.set(key as keyof T, value);
    });
    return this;
  }
  
  addValidator<K extends keyof T>(
    key: K,
    validator: (value: T[K]) => boolean
  ): this {
    this.validators.set(key, validator);
    return this;
  }
  
  build(): T {
    // In a real implementation, would validate required fields
    return this.object as T;
  }
  
  reset(): this {
    this.object = {};
    return this;
  }
}

// ๐Ÿ  Usage example
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  roles: string[];
}

const userBuilder = new Builder<User>()
  .addValidator('email', (email) => email.includes('@'))
  .addValidator('age', (age) => age >= 0 && age <= 150)
  .addValidator('roles', (roles) => roles.length > 0);

const user = userBuilder
  .set('id', '123')
  .set('name', 'John Doe')
  .set('email', '[email protected]')
  .set('age', 30)
  .set('roles', ['user', 'admin'])
  .build();

// ๐Ÿ”ง Fluent builder with method chaining
class FluentBuilder<T, R = {}> {
  constructor(private current: R) {}
  
  add<K extends string, V>(
    key: K,
    value: V
  ): FluentBuilder<T, R & Record<K, V>> {
    return new FluentBuilder({
      ...this.current,
      [key]: value
    });
  }
  
  build(): R {
    return this.current;
  }
}

// Type-safe building
const config = new FluentBuilder<never, {}>({})
  .add('port', 3000)
  .add('host', 'localhost')
  .add('debug', true)
  .build();
// Type is { port: number; host: string; debug: boolean }

๐ŸŽช Real-World Applications

๐Ÿ’พ Generic Cache Implementation

Building a sophisticated caching system:

// ๐ŸŽฏ Cache entry with metadata
interface CacheEntry<T> {
  value: T;
  timestamp: number;
  hits: number;
  size?: number;
}

// ๐Ÿ—๏ธ Advanced generic cache
class Cache<K, V> {
  private cache = new Map<K, CacheEntry<V>>();
  private accessOrder: K[] = [];
  
  constructor(
    private maxSize: number,
    private ttl?: number,
    private sizeCalculator?: (value: V) => number
  ) {}
  
  set(key: K, value: V): void {
    const size = this.sizeCalculator?.(value) || 1;
    
    // Evict if necessary
    while (this.cache.size >= this.maxSize && !this.cache.has(key)) {
      this.evictLRU();
    }
    
    const entry: CacheEntry<V> = {
      value,
      timestamp: Date.now(),
      hits: 0,
      size
    };
    
    this.cache.set(key, entry);
    this.updateAccessOrder(key);
  }
  
  get(key: K): V | undefined {
    const entry = this.cache.get(key);
    
    if (!entry) return undefined;
    
    // Check TTL
    if (this.ttl && Date.now() - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      this.removeFromAccessOrder(key);
      return undefined;
    }
    
    entry.hits++;
    this.updateAccessOrder(key);
    return entry.value;
  }
  
  has(key: K): boolean {
    if (!this.cache.has(key)) return false;
    
    // Check if expired
    const entry = this.cache.get(key)!;
    if (this.ttl && Date.now() - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      this.removeFromAccessOrder(key);
      return false;
    }
    
    return true;
  }
  
  delete(key: K): boolean {
    this.removeFromAccessOrder(key);
    return this.cache.delete(key);
  }
  
  clear(): void {
    this.cache.clear();
    this.accessOrder = [];
  }
  
  private evictLRU(): void {
    const lru = this.accessOrder[0];
    if (lru !== undefined) {
      this.cache.delete(lru);
      this.accessOrder.shift();
    }
  }
  
  private updateAccessOrder(key: K): void {
    this.removeFromAccessOrder(key);
    this.accessOrder.push(key);
  }
  
  private removeFromAccessOrder(key: K): void {
    const index = this.accessOrder.indexOf(key);
    if (index > -1) {
      this.accessOrder.splice(index, 1);
    }
  }
  
  getStats(): {
    size: number;
    hits: number;
    misses: number;
    hitRate: number;
  } {
    let totalHits = 0;
    let totalAccess = 0;
    
    this.cache.forEach(entry => {
      totalHits += entry.hits;
      totalAccess += entry.hits + 1; // +1 for initial set
    });
    
    return {
      size: this.cache.size,
      hits: totalHits,
      misses: totalAccess - totalHits,
      hitRate: totalAccess > 0 ? totalHits / totalAccess : 0
    };
  }
  
  // Iterator support
  *entries(): IterableIterator<[K, V]> {
    for (const [key, entry] of this.cache) {
      if (!this.ttl || Date.now() - entry.timestamp <= this.ttl) {
        yield [key, entry.value];
      }
    }
  }
  
  *[Symbol.iterator](): IterableIterator<[K, V]> {
    yield* this.entries();
  }
}

// ๐Ÿ’ซ Usage examples
// Simple string cache
const stringCache = new Cache<string, string>(100, 60000); // 100 items, 1 minute TTL
stringCache.set('key1', 'value1');
stringCache.set('key2', 'value2');

// Object cache with size calculation
interface CachedData {
  id: string;
  content: string;
  metadata: Record<string, any>;
}

const dataCache = new Cache<string, CachedData>(
  50,
  300000, // 5 minutes
  (data) => JSON.stringify(data).length // Size based on JSON length
);

dataCache.set('user:123', {
  id: '123',
  content: 'User data',
  metadata: { created: new Date(), role: 'admin' }
});

// Iterate over cache
for (const [key, value] of dataCache) {
  console.log(key, value);
}

๐ŸŽฎ State Management

Generic state container with history:

// ๐ŸŽฏ State with history tracking
class StateContainer<T> {
  private currentState: T;
  private history: T[] = [];
  private future: T[] = [];
  private maxHistory: number;
  private listeners = new Set<(state: T, prevState: T) => void>();
  
  constructor(initialState: T, maxHistory: number = 50) {
    this.currentState = initialState;
    this.maxHistory = maxHistory;
  }
  
  getState(): T {
    return this.currentState;
  }
  
  setState(newState: T | ((prev: T) => T)): void {
    const prevState = this.currentState;
    
    this.currentState = typeof newState === 'function'
      ? (newState as (prev: T) => T)(prevState)
      : newState;
    
    // Add to history
    this.history.push(prevState);
    if (this.history.length > this.maxHistory) {
      this.history.shift();
    }
    
    // Clear future (new timeline)
    this.future = [];
    
    // Notify listeners
    this.notifyListeners(prevState);
  }
  
  undo(): boolean {
    if (this.history.length === 0) return false;
    
    const prevState = this.currentState;
    const newState = this.history.pop()!;
    
    this.future.push(prevState);
    this.currentState = newState;
    
    this.notifyListeners(prevState);
    return true;
  }
  
  redo(): boolean {
    if (this.future.length === 0) return false;
    
    const prevState = this.currentState;
    const newState = this.future.pop()!;
    
    this.history.push(prevState);
    this.currentState = newState;
    
    this.notifyListeners(prevState);
    return true;
  }
  
  subscribe(listener: (state: T, prevState: T) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
  
  private notifyListeners(prevState: T): void {
    this.listeners.forEach(listener => listener(this.currentState, prevState));
  }
  
  getHistory(): T[] {
    return [...this.history];
  }
  
  canUndo(): boolean {
    return this.history.length > 0;
  }
  
  canRedo(): boolean {
    return this.future.length > 0;
  }
}

// ๐Ÿ’ซ Usage
interface AppState {
  count: number;
  user: string | null;
  todos: string[];
}

const state = new StateContainer<AppState>({
  count: 0,
  user: null,
  todos: []
});

// Subscribe to changes
state.subscribe((newState, prevState) => {
  console.log('State changed:', { prevState, newState });
});

// Make changes
state.setState(prev => ({
  ...prev,
  count: prev.count + 1
}));

state.setState(prev => ({
  ...prev,
  user: 'John'
}));

state.setState(prev => ({
  ...prev,
  todos: [...prev.todos, 'Learn TypeScript']
}));

// Undo/Redo
console.log('Can undo:', state.canUndo()); // true
state.undo();
console.log('Current state:', state.getState());

state.redo();
console.log('After redo:', state.getState());

๐ŸŽฎ Hands-On Exercise

Letโ€™s build a generic priority queue!

๐Ÿ“ Challenge: Priority Queue Implementation

Create a priority queue that:

  1. Supports any type with a priority
  2. Maintains heap property for O(log n) operations
  3. Provides iteration in priority order
  4. Supports priority updates
// Your challenge: Implement this priority queue
interface PriorityItem<T> {
  value: T;
  priority: number;
}

class PriorityQueue<T> {
  // Implement:
  // - enqueue(item: T, priority: number): void
  // - dequeue(): T | undefined
  // - peek(): T | undefined
  // - updatePriority(item: T, newPriority: number): boolean
  // - size(): number
  // - isEmpty(): boolean
  // - clear(): void
  // - toArray(): T[] (in priority order)
}

// Example usage to support:
interface Task {
  id: string;
  name: string;
}

const taskQueue = new PriorityQueue<Task>();

taskQueue.enqueue({ id: '1', name: 'Low priority' }, 1);
taskQueue.enqueue({ id: '2', name: 'High priority' }, 10);
taskQueue.enqueue({ id: '3', name: 'Medium priority' }, 5);

const highestPriority = taskQueue.dequeue(); // Should return high priority task

๐Ÿ’ก Solution

Click to see the solution
// ๐ŸŽฏ Priority Queue implementation with min-heap
class PriorityQueue<T> {
  private heap: PriorityItem<T>[] = [];
  private itemMap = new Map<T, number>(); // Maps items to their indices
  
  enqueue(value: T, priority: number): void {
    const item: PriorityItem<T> = { value, priority };
    this.heap.push(item);
    const index = this.heap.length - 1;
    this.itemMap.set(value, index);
    this.bubbleUp(index);
  }
  
  dequeue(): T | undefined {
    if (this.isEmpty()) return undefined;
    
    const root = this.heap[0];
    const last = this.heap.pop()!;
    
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.itemMap.set(last.value, 0);
      this.bubbleDown(0);
    }
    
    this.itemMap.delete(root.value);
    return root.value;
  }
  
  peek(): T | undefined {
    return this.heap[0]?.value;
  }
  
  updatePriority(value: T, newPriority: number): boolean {
    const index = this.itemMap.get(value);
    if (index === undefined) return false;
    
    const oldPriority = this.heap[index].priority;
    this.heap[index].priority = newPriority;
    
    // Restore heap property
    if (newPriority > oldPriority) {
      this.bubbleUp(index);
    } else {
      this.bubbleDown(index);
    }
    
    return true;
  }
  
  size(): number {
    return this.heap.length;
  }
  
  isEmpty(): boolean {
    return this.heap.length === 0;
  }
  
  clear(): void {
    this.heap = [];
    this.itemMap.clear();
  }
  
  toArray(): T[] {
    // Return items in priority order without modifying the heap
    const sorted = [...this.heap].sort((a, b) => b.priority - a.priority);
    return sorted.map(item => item.value);
  }
  
  private bubbleUp(index: number): void {
    while (index > 0) {
      const parentIndex = Math.floor((index - 1) / 2);
      
      if (this.heap[parentIndex].priority >= this.heap[index].priority) {
        break;
      }
      
      this.swap(index, parentIndex);
      index = parentIndex;
    }
  }
  
  private bubbleDown(index: number): void {
    while (true) {
      let largest = index;
      const leftChild = 2 * index + 1;
      const rightChild = 2 * index + 2;
      
      if (leftChild < this.heap.length &&
          this.heap[leftChild].priority > this.heap[largest].priority) {
        largest = leftChild;
      }
      
      if (rightChild < this.heap.length &&
          this.heap[rightChild].priority > this.heap[largest].priority) {
        largest = rightChild;
      }
      
      if (largest === index) break;
      
      this.swap(index, largest);
      index = largest;
    }
  }
  
  private swap(i: number, j: number): void {
    const temp = this.heap[i];
    this.heap[i] = this.heap[j];
    this.heap[j] = temp;
    
    // Update map
    this.itemMap.set(this.heap[i].value, i);
    this.itemMap.set(this.heap[j].value, j);
  }
  
  // Iterator support
  *[Symbol.iterator](): Iterator<T> {
    const sorted = this.toArray();
    for (const item of sorted) {
      yield item;
    }
  }
  
  // Debug method
  visualize(): void {
    console.log('Priority Queue Heap:');
    for (let i = 0; i < this.heap.length; i++) {
      const level = Math.floor(Math.log2(i + 1));
      const spaces = ' '.repeat(level * 2);
      console.log(`${spaces}[${this.heap[i].priority}] ${JSON.stringify(this.heap[i].value)}`);
    }
  }
}

// ๐Ÿ—๏ธ Extended priority queue with custom comparator
class CustomPriorityQueue<T> extends PriorityQueue<T> {
  private compareFn: (a: T, b: T) => number;
  
  constructor(compareFn?: (a: T, b: T) => number) {
    super();
    this.compareFn = compareFn || (() => 0);
  }
  
  enqueueBatch(items: Array<{ value: T; priority: number }>): void {
    items.forEach(item => this.enqueue(item.value, item.priority));
  }
  
  dequeueMultiple(count: number): T[] {
    const results: T[] = [];
    for (let i = 0; i < count && !this.isEmpty(); i++) {
      const item = this.dequeue();
      if (item !== undefined) {
        results.push(item);
      }
    }
    return results;
  }
}

// ๐Ÿ’ซ Test the implementation
interface Task {
  id: string;
  name: string;
  createdAt: Date;
}

const taskQueue = new PriorityQueue<Task>();

// Add tasks
taskQueue.enqueue(
  { id: '1', name: 'Write tests', createdAt: new Date() },
  3
);
taskQueue.enqueue(
  { id: '2', name: 'Fix critical bug', createdAt: new Date() },
  10
);
taskQueue.enqueue(
  { id: '3', name: 'Code review', createdAt: new Date() },
  5
);
taskQueue.enqueue(
  { id: '4', name: 'Documentation', createdAt: new Date() },
  2
);

console.log('Tasks in priority order:');
for (const task of taskQueue) {
  console.log(`- ${task.name}`);
}

// Process highest priority
const urgent = taskQueue.dequeue();
console.log('\nProcessing:', urgent?.name);

// Update priority
const docTask = { id: '4', name: 'Documentation', createdAt: new Date() };
taskQueue.updatePriority(docTask, 8);
console.log('\nAfter priority update:');
taskQueue.visualize();

// Test with different types
const numberQueue = new PriorityQueue<number>();
[42, 17, 35, 8, 91].forEach((num, i) => {
  numberQueue.enqueue(num, num); // Using number itself as priority
});

console.log('\nNumbers by priority:', numberQueue.toArray());

๐ŸŽฏ Summary

Youโ€™ve mastered generic classes in TypeScript! ๐ŸŽ‰ You learned how to:

  • ๐Ÿ“ฆ Create flexible, reusable class-based data structures
  • ๐ŸŒณ Build complex generic collections like trees and queues
  • ๐Ÿ”„ Implement observable and reactive patterns
  • ๐Ÿ’พ Design sophisticated caching systems
  • ๐ŸŽฎ Create state management solutions with history
  • โœจ Maintain complete type safety throughout

Generic classes are fundamental for building professional TypeScript libraries and applications. They enable you to create reusable components that work with any type while providing excellent developer experience!

Keep building amazing generic data structures! ๐Ÿš€