+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 57 of 72

๐Ÿ”ง Generic Functions: Type-Safe Utility Functions

Master generic functions in TypeScript to create powerful, reusable utility functions that maintain type safety ๐Ÿš€

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Understanding of TypeScript generics basics ๐Ÿ“
  • Knowledge of function types ๐Ÿ”
  • Familiarity with type inference ๐Ÿ’ป

What you'll learn

  • Create advanced generic utility functions ๐ŸŽฏ
  • Master type inference in generic contexts ๐Ÿ—๏ธ
  • Build complex generic function signatures ๐Ÿ›ก๏ธ
  • Implement real-world utility libraries โœจ

๐ŸŽฏ Introduction

Welcome to the workshop of generic functions! ๐ŸŽ‰ In this guide, weโ€™ll dive deep into creating powerful utility functions that work with any type while maintaining complete type safety.

Youโ€™ll discover how generic functions are like Swiss Army knives ๐Ÿ”ง - versatile tools that adapt to any situation! Whether youโ€™re building utility libraries ๐Ÿ“š, data transformations ๐Ÿ”„, or complex algorithms ๐Ÿงฎ, mastering generic functions is essential for writing professional TypeScript code.

By the end of this tutorial, youโ€™ll be confidently creating generic functions that solve real-world problems elegantly and safely! Letโ€™s build some powerful utilities! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Generic Function Fundamentals

๐Ÿค” Beyond Basic Generics

Generic functions go beyond simple type parameters - they enable sophisticated type relationships and constraints:

// ๐ŸŽฏ Basic generic function review
function identity<T>(value: T): T {
  return value;
}

// ๐Ÿ—๏ธ Multiple type parameters with relationships
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

const original: [string, number] = ['hello', 42];
const swapped = swap(original); // [number, string]

// ๐Ÿ”ง Generic functions with type inference
function makeArray<T>(...items: T[]): T[] {
  return items;
}

// TypeScript infers the common type
const mixed = makeArray(1, 2, 3); // number[]
const strings = makeArray('a', 'b', 'c'); // string[]

// ๐ŸŽจ Constraining generic functions
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const person = { name: 'Alice', age: 30 };
const job = { title: 'Developer', company: 'TechCorp' };
const employee = merge(person, job);
// Type is { name: string; age: number; title: string; company: string; }

๐Ÿ’ก Type Parameter Inference

Understanding how TypeScript infers types in generic functions:

// ๐ŸŽฏ Inference from arguments
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Type is inferred from the argument
const first1 = firstElement([1, 2, 3]); // number | undefined
const first2 = firstElement(['a', 'b', 'c']); // string | undefined

// ๐Ÿ—๏ธ Inference with constraints
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

// Works with anything that has a length property
const strLen = getLength('hello'); // string inferred
const arrLen = getLength([1, 2, 3]); // number[] inferred
const customLen = getLength({ length: 42, value: 'custom' }); // custom type inferred

// ๐Ÿ”ง Conditional return types
function processValue<T>(value: T): T extends string ? string[] : T {
  if (typeof value === 'string') {
    return value.split('') as any;
  }
  return value as any;
}

const processed1 = processValue('hello'); // string[]
const processed2 = processValue(42); // number
const processed3 = processValue({ key: 'value' }); // { key: string }

๐Ÿš€ Advanced Generic Patterns

๐ŸŽจ Function Composition

Building type-safe function composition:

// ๐ŸŽฏ Basic pipe function
function pipe<A, B>(
  fn1: (a: A) => B
): (a: A) => B;
function pipe<A, B, C>(
  fn1: (a: A) => B,
  fn2: (b: B) => C
): (a: A) => C;
function pipe<A, B, C, D>(
  fn1: (a: A) => B,
  fn2: (b: B) => C,
  fn3: (c: C) => D
): (a: A) => D;
function pipe(...fns: Function[]) {
  return (value: any) => fns.reduce((acc, fn) => fn(acc), value);
}

// ๐Ÿ’ซ Usage with type safety
const addOne = (n: number) => n + 1;
const double = (n: number) => n * 2;
const toString = (n: number) => n.toString();

const pipeline = pipe(addOne, double, toString);
const result = pipeline(5); // "12" - Type is string!

// ๐Ÿ—๏ธ Compose function (right to left)
function compose<A, B>(
  fn1: (a: A) => B
): (a: A) => B;
function compose<A, B, C>(
  fn2: (b: B) => C,
  fn1: (a: A) => B
): (a: A) => C;
function compose<A, B, C, D>(
  fn3: (c: C) => D,
  fn2: (b: B) => C,
  fn1: (a: A) => B
): (a: A) => D;
function compose(...fns: Function[]) {
  return (value: any) => fns.reduceRight((acc, fn) => fn(acc), value);
}

// ๐Ÿ”ง Advanced composition with async
async function pipeAsync<A, B>(
  value: A,
  fn1: (a: A) => B | Promise<B>
): Promise<B>;
async function pipeAsync<A, B, C>(
  value: A,
  fn1: (a: A) => B | Promise<B>,
  fn2: (b: B) => C | Promise<C>
): Promise<C>;
async function pipeAsync<A, B, C, D>(
  value: A,
  fn1: (a: A) => B | Promise<B>,
  fn2: (b: B) => C | Promise<C>,
  fn3: (c: C) => D | Promise<D>
): Promise<D>;
async function pipeAsync(value: any, ...fns: Function[]) {
  let result = value;
  for (const fn of fns) {
    result = await fn(result);
  }
  return result;
}

// Usage
const fetchUser = async (id: string) => ({ id, name: 'Alice' });
const enrichUser = async (user: { id: string; name: string }) => 
  ({ ...user, email: `${user.name.toLowerCase()}@example.com` });
const formatUser = (user: any) => `${user.name} <${user.email}>`;

const formattedUser = await pipeAsync(
  '123',
  fetchUser,
  enrichUser,
  formatUser
); // "Alice <[email protected]>"

๐Ÿ”„ Higher-Order Generic Functions

Functions that return generic functions:

// ๐ŸŽฏ Curry function with generics
function curry<A, B, C>(
  fn: (a: A, b: B) => C
): (a: A) => (b: B) => C {
  return (a: A) => (b: B) => fn(a, b);
}

function curry3<A, B, C, D>(
  fn: (a: A, b: B, c: C) => D
): (a: A) => (b: B) => (c: C) => D {
  return (a: A) => (b: B) => (c: C) => fn(a, b, c);
}

// Usage
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
console.log(add5(3)); // 8

// ๐Ÿ—๏ธ Partial application
function partial<A, B, C>(
  fn: (a: A, b: B) => C,
  a: A
): (b: B) => C {
  return (b: B) => fn(a, b);
}

function partial2<A, B, C, D>(
  fn: (a: A, b: B, c: C) => D,
  a: A,
  b: B
): (c: C) => D {
  return (c: C) => fn(a, b, c);
}

// ๐Ÿ”ง Memoization with generics
function memoize<TArgs extends any[], TReturn>(
  fn: (...args: TArgs) => TReturn,
  keyGenerator?: (...args: TArgs) => string
): (...args: TArgs) => TReturn {
  const cache = new Map<string, TReturn>();
  
  return (...args: TArgs): TReturn => {
    const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key)!;
    }
    
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// Complex calculation
const fibonacci = memoize((n: number): number => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(40)); // Fast due to memoization!

// ๐ŸŽจ Debounce with generics
function debounce<TArgs extends any[]>(
  fn: (...args: TArgs) => void,
  delay: number
): (...args: TArgs) => void {
  let timeoutId: NodeJS.Timeout;
  
  return (...args: TArgs) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Type-safe event handler
const handleSearch = debounce((query: string, filters?: { category: string }) => {
  console.log('Searching:', query, filters);
}, 300);

handleSearch('typescript', { category: 'programming' }); // Fully typed!

๐ŸŽช Real-World Utility Functions

๐Ÿ“Š Array Utilities

Advanced array manipulation with generics:

// ๐ŸŽฏ Group by with type safety
function groupBy<T, K extends keyof T>(
  array: T[],
  key: K
): Record<string, T[]> {
  return array.reduce((groups, item) => {
    const groupKey = String(item[key]);
    if (!groups[groupKey]) {
      groups[groupKey] = [];
    }
    groups[groupKey].push(item);
    return groups;
  }, {} as Record<string, T[]>);
}

// Advanced groupBy with custom key function
function groupByFn<T, K extends string | number>(
  array: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  return array.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
    return groups;
  }, {} as Record<K, T[]>);
}

// Usage
interface Person {
  name: string;
  age: number;
  department: string;
}

const people: Person[] = [
  { name: 'Alice', age: 30, department: 'Engineering' },
  { name: 'Bob', age: 25, department: 'Engineering' },
  { name: 'Charlie', age: 35, department: 'Marketing' }
];

const byDepartment = groupBy(people, 'department');
const byAgeGroup = groupByFn(people, p => Math.floor(p.age / 10) * 10);

// ๐Ÿ—๏ธ Partition array
function partition<T>(
  array: T[],
  predicate: (item: T, index: number) => boolean
): [T[], T[]] {
  const pass: T[] = [];
  const fail: T[] = [];
  
  array.forEach((item, index) => {
    if (predicate(item, index)) {
      pass.push(item);
    } else {
      fail.push(item);
    }
  });
  
  return [pass, fail];
}

const [adults, minors] = partition(people, p => p.age >= 18);

// ๐Ÿ”ง Unique by property
function uniqueBy<T, K extends keyof T>(
  array: T[],
  key: K
): T[] {
  const seen = new Set<T[K]>();
  return array.filter(item => {
    const value = item[key];
    if (seen.has(value)) {
      return false;
    }
    seen.add(value);
    return true;
  });
}

// Or with custom selector
function uniqueByFn<T, U>(
  array: T[],
  selector: (item: T) => U
): T[] {
  const seen = new Set<U>();
  return array.filter(item => {
    const value = selector(item);
    if (seen.has(value)) {
      return false;
    }
    seen.add(value);
    return true;
  });
}

// ๐ŸŽจ Chunk array
function chunk<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const chunks = chunk(numbers, 3); // [[1,2,3], [4,5,6], [7,8,9]]

๐Ÿ” Object Utilities

Type-safe object manipulation:

// ๐ŸŽฏ Deep pick utility
type DeepPick<T, K extends string> = K extends `${infer P}.${infer R}`
  ? P extends keyof T
    ? { [K in P]: DeepPick<T[P], R> }
    : never
  : K extends keyof T
    ? { [K2 in K]: T[K2] }
    : never;

function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    if (key in obj) {
      result[key] = obj[key];
    }
  });
  return result;
}

// ๐Ÿ—๏ธ Deep merge
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

function deepMerge<T extends object>(
  target: T,
  ...sources: DeepPartial<T>[]
): T {
  if (!sources.length) return target;
  
  const source = sources.shift();
  if (!source) return target;
  
  for (const key in source) {
    const sourceValue = source[key];
    const targetValue = target[key];
    
    if (isObject(targetValue) && isObject(sourceValue)) {
      target[key] = deepMerge(targetValue, sourceValue as any);
    } else if (sourceValue !== undefined) {
      target[key] = sourceValue as any;
    }
  }
  
  return deepMerge(target, ...sources);
}

function isObject(item: any): item is object {
  return item && typeof item === 'object' && !Array.isArray(item);
}

// ๐Ÿ”ง Object mapping with type transformation
function mapValues<T extends object, U>(
  obj: T,
  fn: <K extends keyof T>(value: T[K], key: K) => U
): { [K in keyof T]: U } {
  const result = {} as { [K in keyof T]: U };
  
  for (const key in obj) {
    result[key] = fn(obj[key], key);
  }
  
  return result;
}

// Usage
const prices = { apple: 1.5, banana: 0.8, orange: 2.0 };
const formatted = mapValues(prices, price => `$${price.toFixed(2)}`);
// { apple: "$1.50", banana: "$0.80", orange: "$2.00" }

// ๐ŸŽจ Safe object path access
function get<T, K extends string>(
  obj: T,
  path: K,
  defaultValue?: any
): any {
  const keys = path.split('.');
  let result: any = obj;
  
  for (const key of keys) {
    if (result == null) {
      return defaultValue;
    }
    result = result[key];
  }
  
  return result ?? defaultValue;
}

// Type-safe version with template literals (simplified)
function getPath<T, P extends string>(
  obj: T,
  path: P
): P extends keyof T ? T[P] : any {
  return get(obj, path);
}

๐Ÿ”„ Async Utilities

Generic async function utilities:

// ๐ŸŽฏ Retry with exponential backoff
async function retry<T>(
  fn: () => Promise<T>,
  options: {
    attempts?: number;
    delay?: number;
    backoff?: number;
    onError?: (error: Error, attempt: number) => void;
  } = {}
): Promise<T> {
  const {
    attempts = 3,
    delay = 1000,
    backoff = 2,
    onError
  } = options;
  
  let lastError: Error;
  
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      onError?.(lastError, i + 1);
      
      if (i < attempts - 1) {
        const waitTime = delay * Math.pow(backoff, i);
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }
  }
  
  throw lastError!;
}

// Usage
const fetchData = () => fetch('/api/data').then(r => r.json());
const data = await retry(fetchData, {
  attempts: 5,
  delay: 500,
  onError: (error, attempt) => {
    console.log(`Attempt ${attempt} failed:`, error.message);
  }
});

// ๐Ÿ—๏ธ Promise timeout wrapper
function withTimeout<T>(
  promise: Promise<T>,
  timeout: number,
  errorMessage = 'Operation timed out'
): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error(errorMessage)), timeout)
    )
  ]);
}

// ๐Ÿ”ง Batch async operations
async function batchAsync<T, R>(
  items: T[],
  batchSize: number,
  fn: (item: T) => Promise<R>
): Promise<R[]> {
  const results: R[] = [];
  
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(fn));
    results.push(...batchResults);
  }
  
  return results;
}

// Process large array in batches
const ids = Array.from({ length: 1000 }, (_, i) => i);
const results = await batchAsync(ids, 10, async (id) => {
  // Process each ID
  return { id, processed: true };
});

// ๐ŸŽจ Async queue with concurrency control
class AsyncQueue<T, R> {
  private queue: Array<() => Promise<R>> = [];
  private running = 0;
  
  constructor(
    private concurrency: number,
    private processor: (item: T) => Promise<R>
  ) {}
  
  async add(item: T): Promise<R> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await this.processor(item);
          resolve(result);
          return result;
        } catch (error) {
          reject(error);
          throw error;
        }
      });
      
      this.process();
    });
  }
  
  private async process() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return;
    }
    
    this.running++;
    const task = this.queue.shift()!;
    
    try {
      await task();
    } finally {
      this.running--;
      this.process();
    }
  }
}

// Usage
const downloadQueue = new AsyncQueue(3, async (url: string) => {
  const response = await fetch(url);
  return response.blob();
});

// Only 3 downloads will run concurrently
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const downloads = await Promise.all(urls.map(url => downloadQueue.add(url)));

๐ŸŽฎ Hands-On Exercise

Letโ€™s build a type-safe event system using generic functions!

๐Ÿ“ Challenge: Generic Event Emitter

Create an event emitter that:

  1. Provides type-safe event registration and emission
  2. Supports wildcard listeners
  3. Includes once() functionality
  4. Has proper event typing
// Your challenge: Implement this event system
type EventMap = Record<string, any[]>;

interface Emitter<T extends EventMap> {
  on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void;
  off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void;
  emit<K extends keyof T>(event: K, ...args: T[K]): void;
  once<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void;
  onAny(listener: (event: keyof T, ...args: any[]) => void): void;
  offAny(listener: (event: keyof T, ...args: any[]) => void): void;
  clear(event?: keyof T): void;
}

// Example usage to support:
interface GameEvents {
  start: [];
  end: [score: number, winner: string];
  move: [x: number, y: number];
  powerup: [type: string, duration: number];
}

const game = createEmitter<GameEvents>();

game.on('end', (score, winner) => {
  console.log(`Game ended! ${winner} won with ${score} points`);
});

game.onAny((event, ...args) => {
  console.log(`Event ${event} fired with args:`, args);
});

game.emit('end', 100, 'Player 1'); // Type-safe!

๐Ÿ’ก Solution

Click to see the solution
// ๐ŸŽฏ Type-safe event emitter implementation
function createEmitter<T extends EventMap>(): Emitter<T> {
  const listeners = new Map<keyof T, Set<Function>>();
  const onceListeners = new Map<keyof T, Set<Function>>();
  const anyListeners = new Set<(event: keyof T, ...args: any[]) => void>();
  
  return {
    on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
      if (!listeners.has(event)) {
        listeners.set(event, new Set());
      }
      listeners.get(event)!.add(listener);
    },
    
    off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
      listeners.get(event)?.delete(listener);
      onceListeners.get(event)?.delete(listener);
    },
    
    emit<K extends keyof T>(event: K, ...args: T[K]): void {
      // Emit to specific listeners
      listeners.get(event)?.forEach(listener => {
        listener(...args);
      });
      
      // Emit to once listeners
      const onceSet = onceListeners.get(event);
      if (onceSet) {
        onceSet.forEach(listener => {
          listener(...args);
        });
        onceSet.clear();
      }
      
      // Emit to any listeners
      anyListeners.forEach(listener => {
        listener(event, ...args);
      });
    },
    
    once<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
      if (!onceListeners.has(event)) {
        onceListeners.set(event, new Set());
      }
      onceListeners.get(event)!.add(listener);
    },
    
    onAny(listener: (event: keyof T, ...args: any[]) => void): void {
      anyListeners.add(listener);
    },
    
    offAny(listener: (event: keyof T, ...args: any[]) => void): void {
      anyListeners.delete(listener);
    },
    
    clear(event?: keyof T): void {
      if (event) {
        listeners.delete(event);
        onceListeners.delete(event);
      } else {
        listeners.clear();
        onceListeners.clear();
        anyListeners.clear();
      }
    }
  };
}

// ๐Ÿ—๏ธ Advanced features
function createAdvancedEmitter<T extends EventMap>() {
  const basicEmitter = createEmitter<T>();
  const eventHistory: Array<{ event: keyof T; args: any[]; timestamp: Date }> = [];
  
  return {
    ...basicEmitter,
    
    // Emit with history tracking
    emit<K extends keyof T>(event: K, ...args: T[K]): void {
      eventHistory.push({ event, args, timestamp: new Date() });
      basicEmitter.emit(event, ...args);
    },
    
    // Get event history
    getHistory(event?: keyof T) {
      return event
        ? eventHistory.filter(e => e.event === event)
        : [...eventHistory];
    },
    
    // Replay events
    replay(event?: keyof T, since?: Date): void {
      const events = this.getHistory(event);
      
      events
        .filter(e => !since || e.timestamp >= since)
        .forEach(e => {
          basicEmitter.emit(e.event, ...e.args as any);
        });
    },
    
    // Wait for event (promise-based)
    waitFor<K extends keyof T>(event: K, timeout?: number): Promise<T[K]> {
      return new Promise((resolve, reject) => {
        const timeoutId = timeout ? setTimeout(() => {
          basicEmitter.off(event, handler);
          reject(new Error(`Timeout waiting for event ${String(event)}`));
        }, timeout) : null;
        
        const handler = (...args: T[K]) => {
          if (timeoutId) clearTimeout(timeoutId);
          resolve(args);
        };
        
        basicEmitter.once(event, handler);
      });
    },
    
    // Pipe events to another emitter
    pipe<U extends EventMap>(
      target: Emitter<U>,
      mapping?: Partial<Record<keyof T, keyof U>>
    ): () => void {
      const handler = (event: keyof T, ...args: any[]) => {
        const targetEvent = mapping?.[event] || event;
        if (targetEvent in target) {
          (target as any).emit(targetEvent, ...args);
        }
      };
      
      basicEmitter.onAny(handler);
      return () => basicEmitter.offAny(handler);
    }
  };
}

// ๐Ÿ’ซ Test the implementation
interface GameEvents {
  start: [];
  end: [score: number, winner: string];
  move: [x: number, y: number];
  powerup: [type: string, duration: number];
  damage: [amount: number, source: string];
}

interface UIEvents {
  click: [x: number, y: number];
  keypress: [key: string];
  resize: [width: number, height: number];
}

async function testEventSystem() {
  console.log('=== Event System Test ===\n');
  
  const game = createAdvancedEmitter<GameEvents>();
  const ui = createAdvancedEmitter<UIEvents>();
  
  // Set up listeners
  game.on('start', () => console.log('๐ŸŽฎ Game started!'));
  
  game.on('end', (score, winner) => {
    console.log(`๐Ÿ† Game ended! ${winner} won with ${score} points`);
  });
  
  game.on('move', (x, y) => {
    console.log(`๐Ÿ“ Moved to (${x}, ${y})`);
  });
  
  game.once('powerup', (type, duration) => {
    console.log(`โšก First powerup: ${type} for ${duration}s`);
  });
  
  game.onAny((event, ...args) => {
    console.log(`๐Ÿ“ข [${event}]`, args);
  });
  
  // Pipe game events to UI (with mapping)
  const unpipe = game.pipe(ui, {
    move: 'click' // Map game moves to UI clicks
  });
  
  ui.on('click', (x, y) => {
    console.log(`๐Ÿ–ฑ๏ธ UI Click at (${x}, ${y})`);
  });
  
  // Emit events
  game.emit('start');
  game.emit('move', 10, 20);
  game.emit('powerup', 'speed', 10);
  game.emit('powerup', 'shield', 5); // Won't trigger once listener
  
  // Wait for event
  console.log('\nโณ Waiting for damage event...');
  setTimeout(() => game.emit('damage', 50, 'enemy'), 1000);
  
  const [damage, source] = await game.waitFor('damage', 2000);
  console.log(`๐Ÿ’ฅ Received damage: ${damage} from ${source}`);
  
  // Replay events
  console.log('\n๐Ÿ”„ Replaying all events:');
  game.replay();
  
  // Clean up
  unpipe();
  game.clear();
  
  console.log('\nโœ… Test complete!');
}

testEventSystem();

๐ŸŽฏ Summary

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

  • ๐Ÿ”ง Create advanced generic utility functions
  • ๐ŸŽจ Build type-safe function composition
  • ๐Ÿ“Š Implement complex array and object utilities
  • ๐Ÿ”„ Handle async operations with generics
  • ๐Ÿ—๏ธ Design flexible, reusable function libraries
  • โœจ Maintain complete type safety in generic contexts

Generic functions are the backbone of type-safe utility libraries and enable you to write code that is both flexible and reliable. Youโ€™re now equipped to build professional-grade TypeScript utilities!

Keep building powerful generic functions! ๐Ÿš€