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:
- Provides type-safe event registration and emission
- Supports wildcard listeners
- Includes once() functionality
- 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! ๐