Prerequisites
- Strong understanding of TypeScript generics 📝
- Knowledge of generic functions and classes 🔍
- Familiarity with type constraints 💻
What you'll learn
- Create complex multi-parameter generic types 🎯
- Build sophisticated type relationships 🏗️
- Master type parameter constraints and inference 🛡️
- Design professional generic libraries ✨
🎯 Introduction
Welcome to the advanced world of multiple type parameters! 🎉 In this guide, we’ll explore how to create sophisticated generic types that work with multiple type parameters to build complex, flexible, and type-safe systems.
You’ll discover how multiple type parameters are like multi-dimensional building blocks 🧩 - they allow you to create intricate type relationships and patterns! Whether you’re building advanced utilities 🔧, designing complex APIs 🌐, or creating professional libraries 📚, mastering multiple type parameters is essential for advanced TypeScript development.
By the end of this tutorial, you’ll be confidently creating complex generic types that elegantly solve real-world problems! Let’s dive into advanced generic patterns! 🏊♂️
📚 Understanding Multiple Type Parameters
🤔 Beyond Single Type Parameters
Multiple type parameters enable you to create sophisticated relationships between types:
// ❌ Limited with single type parameter
interface SimpleMap<T> {
get(key: string): T;
set(key: string, value: T): void;
}
// Can only handle string keys! 😢
// ✅ Flexible with multiple type parameters
interface FlexibleMap<K, V> {
get(key: K): V | undefined;
set(key: K, value: V): void;
has(key: K): boolean;
delete(key: K): boolean;
}
// Now works with any key and value types!
const numberMap: FlexibleMap<number, string> = new Map();
const symbolMap: FlexibleMap<symbol, User> = new Map();
const objectMap: FlexibleMap<{ id: string }, boolean> = new Map();
// 🎯 Type parameters can reference each other
interface Converter<TInput, TOutput> {
convert(input: TInput): TOutput;
convertMany(inputs: TInput[]): TOutput[];
canConvert(value: unknown): value is TInput;
}
// 🏗️ Complex relationships between parameters
interface Relationship<TParent, TChild> {
parent: TParent;
children: TChild[];
addChild(child: TChild): void;
removeChild(child: TChild): boolean;
findChild(predicate: (child: TChild) => boolean): TChild | undefined;
}
💡 Type Parameter Ordering and Naming
Best practices for multiple type parameters:
// 🎯 Conventional ordering and naming
// 1. Input types before output types
// 2. More general types before specific types
// 3. Meaningful names for clarity
interface Transform<TInput, TOutput, TOptions = {}> {
transform(input: TInput, options?: TOptions): TOutput;
}
interface AsyncProcessor<
TRequest,
TResponse,
TError = Error,
TContext = {}
> {
process(
request: TRequest,
context?: TContext
): Promise<TResponse>;
handleError?(
error: TError,
request: TRequest,
context?: TContext
): TResponse | Promise<TResponse>;
}
// 🏗️ Descriptive names for complex scenarios
interface DataMigration<
TSourceSchema,
TTargetSchema,
TMigrationContext = {}
> {
migrate(
source: TSourceSchema,
context?: TMigrationContext
): TTargetSchema;
rollback(
target: TTargetSchema,
context?: TMigrationContext
): TSourceSchema;
}
// 🔧 Default type parameters
interface Cache<
TKey = string,
TValue = any,
TMetadata = { timestamp: Date }
> {
get(key: TKey): TValue | undefined;
set(key: TKey, value: TValue, metadata?: TMetadata): void;
getMetadata(key: TKey): TMetadata | undefined;
}
// Using defaults
const simpleCache: Cache = new CacheImpl(); // Uses all defaults
const customCache: Cache<number, User> = new CacheImpl(); // Override some
🚀 Advanced Constraint Patterns
🔗 Interdependent Constraints
Creating complex relationships between type parameters:
// 🎯 Constraints that reference other type parameters
interface Mapper<T, U extends keyof T, V> {
map(obj: T, key: U, value: V): T & { [K in U]: V };
}
// 🏗️ Nested constraint relationships
interface NestedAccessor<
T,
K1 extends keyof T,
K2 extends keyof T[K1]
> {
get(obj: T, key1: K1, key2: K2): T[K1][K2];
set(obj: T, key1: K1, key2: K2, value: T[K1][K2]): void;
}
// Usage
interface Person {
name: string;
address: {
street: string;
city: string;
country: string;
};
contacts: {
email: string;
phone: string;
};
}
const accessor: NestedAccessor<Person, 'address', 'city'> = {
get(person, key1, key2) {
return person[key1][key2];
},
set(person, key1, key2, value) {
person[key1][key2] = value;
}
};
// 🔧 Conditional constraints
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface TypedSelector<T, U> {
select<K extends KeysOfType<T, U>>(
obj: T,
key: K
): T[K];
selectAll<K extends KeysOfType<T, U>>(
obj: T
): Pick<T, K>;
}
// 🎨 Complex conditional relationships
interface ConditionalTransform<T, U, V> {
transform: T extends U ? (value: T) => V : never;
canTransform: (value: unknown) => value is T;
reverseTransform?: V extends T ? (value: V) => T : never;
}
🎭 Higher-Order Type Parameters
Working with generic types that accept generic types:
// 🎯 Generic type that works with other generics
interface Container<T> {
value: T;
}
interface Processor<
TContainer extends Container<any>,
TResult
> {
process<T>(
container: TContainer & Container<T>
): TResult;
}
// 🏗️ Type parameter extraction
type ExtractContainerType<T> = T extends Container<infer U> ? U : never;
interface UnwrapProcessor<
TContainer extends Container<any>
> {
unwrap(container: TContainer): ExtractContainerType<TContainer>;
wrap<T>(value: T): Container<T>;
}
// 🔧 Generic function signatures as parameters
interface Pipeline<
TFn1 extends (...args: any[]) => any,
TFn2 extends (arg: ReturnType<TFn1>) => any,
TFn3 extends (arg: ReturnType<TFn2>) => any = never
> {
pipe: TFn3 extends never
? (fn1: TFn1, fn2: TFn2) => (...args: Parameters<TFn1>) => ReturnType<TFn2>
: (fn1: TFn1, fn2: TFn2, fn3: TFn3) => (...args: Parameters<TFn1>) => ReturnType<TFn3>;
}
// 🎨 Recursive type parameters
interface TreeNode<TValue, TChildren extends TreeNode<any, any> = TreeNode<TValue, any>> {
value: TValue;
children: TChildren[];
}
interface TreeProcessor<
TNode extends TreeNode<any, any>,
TValue = TNode extends TreeNode<infer V, any> ? V : never
> {
traverse(root: TNode, visitor: (value: TValue) => void): void;
find(root: TNode, predicate: (value: TValue) => boolean): TNode | null;
map<TNewValue>(
root: TNode,
transform: (value: TValue) => TNewValue
): TreeNode<TNewValue>;
}
🎪 Real-World Patterns
🔄 Generic State Machines
Building type-safe state machines with multiple parameters:
// 🎯 State machine with states, events, and context
interface StateMachine<
TState extends string,
TEvent extends { type: string },
TContext = {}
> {
currentState: TState;
context: TContext;
transition(event: TEvent): void;
canTransition(event: TEvent): boolean;
onStateChange(listener: (newState: TState, oldState: TState) => void): void;
}
// 🏗️ State configuration
interface StateConfig<
TState extends string,
TEvent extends { type: string },
TContext
> {
initial: TState;
context: TContext;
states: {
[K in TState]: {
on?: {
[E in TEvent['type']]?: {
target: TState;
guard?: (context: TContext, event: TEvent) => boolean;
action?: (context: TContext, event: TEvent) => void;
};
};
entry?: (context: TContext) => void;
exit?: (context: TContext) => void;
};
};
}
// Implementation
class StateMachineImpl<
TState extends string,
TEvent extends { type: string },
TContext
> implements StateMachine<TState, TEvent, TContext> {
currentState: TState;
context: TContext;
private listeners: Array<(newState: TState, oldState: TState) => void> = [];
constructor(private config: StateConfig<TState, TEvent, TContext>) {
this.currentState = config.initial;
this.context = config.context;
this.enterState(this.currentState);
}
transition(event: TEvent): void {
const stateConfig = this.config.states[this.currentState];
const transition = stateConfig.on?.[event.type];
if (!transition) return;
if (transition.guard && !transition.guard(this.context, event)) {
return;
}
const oldState = this.currentState;
// Exit current state
this.exitState(oldState);
// Execute action
transition.action?.(this.context, event);
// Enter new state
this.currentState = transition.target;
this.enterState(this.currentState);
// Notify listeners
this.notifyListeners(this.currentState, oldState);
}
canTransition(event: TEvent): boolean {
const stateConfig = this.config.states[this.currentState];
const transition = stateConfig.on?.[event.type];
if (!transition) return false;
return !transition.guard || transition.guard(this.context, event);
}
onStateChange(listener: (newState: TState, oldState: TState) => void): void {
this.listeners.push(listener);
}
private enterState(state: TState): void {
this.config.states[state].entry?.(this.context);
}
private exitState(state: TState): void {
this.config.states[state].exit?.(this.context);
}
private notifyListeners(newState: TState, oldState: TState): void {
this.listeners.forEach(listener => listener(newState, oldState));
}
}
// Usage example
type TrafficLightState = 'red' | 'yellow' | 'green';
type TrafficLightEvent =
| { type: 'TIMER' }
| { type: 'EMERGENCY' }
| { type: 'RESET' };
interface TrafficLightContext {
emergencyMode: boolean;
cycleCount: number;
}
const trafficLight = new StateMachineImpl<
TrafficLightState,
TrafficLightEvent,
TrafficLightContext
>({
initial: 'red',
context: { emergencyMode: false, cycleCount: 0 },
states: {
red: {
on: {
TIMER: {
target: 'green',
guard: (ctx) => !ctx.emergencyMode,
action: (ctx) => ctx.cycleCount++
},
EMERGENCY: {
target: 'red',
action: (ctx) => ctx.emergencyMode = true
}
},
entry: () => console.log('🔴 Red light')
},
yellow: {
on: {
TIMER: { target: 'red' },
EMERGENCY: {
target: 'red',
action: (ctx) => ctx.emergencyMode = true
}
},
entry: () => console.log('🟡 Yellow light')
},
green: {
on: {
TIMER: { target: 'yellow' },
EMERGENCY: {
target: 'red',
action: (ctx) => ctx.emergencyMode = true
}
},
entry: () => console.log('🟢 Green light')
}
}
});
🗺️ Generic Data Mappers
Complex data transformation with multiple type parameters:
// 🎯 Bidirectional mapper
interface BidirectionalMapper<TSource, TTarget, TContext = {}> {
map(source: TSource, context?: TContext): TTarget;
reverseMap(target: TTarget, context?: TContext): TSource;
mapMany(sources: TSource[], context?: TContext): TTarget[];
reversemapMany(targets: TTarget[], context?: TContext): TSource[];
}
// 🏗️ Field mapping configuration
interface FieldMapping<
TSource,
TTarget,
TSourceField extends keyof TSource,
TTargetField extends keyof TTarget
> {
from: TSourceField;
to: TTargetField;
transform?: (value: TSource[TSourceField]) => TTarget[TTargetField];
reverseTransform?: (value: TTarget[TTargetField]) => TSource[TSourceField];
condition?: (source: TSource) => boolean;
}
// 🔧 Mapper builder
class MapperBuilder<TSource, TTarget, TContext = {}> {
private fieldMappings: Array<FieldMapping<TSource, TTarget, any, any>> = [];
private customMappers: Array<(source: TSource, target: Partial<TTarget>, context?: TContext) => void> = [];
field<
TSourceField extends keyof TSource,
TTargetField extends keyof TTarget
>(
from: TSourceField,
to: TTargetField,
transform?: (value: TSource[TSourceField]) => TTarget[TTargetField]
): this {
this.fieldMappings.push({ from, to, transform });
return this;
}
custom(
mapper: (source: TSource, target: Partial<TTarget>, context?: TContext) => void
): this {
this.customMappers.push(mapper);
return this;
}
build(): BidirectionalMapper<TSource, TTarget, TContext> {
return new MapperImpl(this.fieldMappings, this.customMappers);
}
}
class MapperImpl<TSource, TTarget, TContext>
implements BidirectionalMapper<TSource, TTarget, TContext> {
constructor(
private fieldMappings: Array<FieldMapping<TSource, TTarget, any, any>>,
private customMappers: Array<(source: TSource, target: Partial<TTarget>, context?: TContext) => void>
) {}
map(source: TSource, context?: TContext): TTarget {
const target: Partial<TTarget> = {};
// Apply field mappings
for (const mapping of this.fieldMappings) {
if (mapping.condition && !mapping.condition(source)) {
continue;
}
const value = source[mapping.from];
target[mapping.to] = mapping.transform
? mapping.transform(value)
: value as any;
}
// Apply custom mappers
for (const mapper of this.customMappers) {
mapper(source, target, context);
}
return target as TTarget;
}
reverseMap(target: TTarget, context?: TContext): TSource {
// Implementation for reverse mapping
throw new Error('Reverse mapping not implemented');
}
mapMany(sources: TSource[], context?: TContext): TTarget[] {
return sources.map(source => this.map(source, context));
}
reversemapMany(targets: TTarget[], context?: TContext): TSource[] {
return targets.map(target => this.reverseMap(target, context));
}
}
// Usage
interface UserEntity {
id: number;
firstName: string;
lastName: string;
emailAddress: string;
dateOfBirth: Date;
isActive: boolean;
}
interface UserDTO {
id: string;
fullName: string;
email: string;
age: number;
status: 'active' | 'inactive';
}
const userMapper = new MapperBuilder<UserEntity, UserDTO>()
.field('id', 'id', (id) => id.toString())
.field('emailAddress', 'email')
.field('isActive', 'status', (active) => active ? 'active' : 'inactive')
.custom((source, target) => {
target.fullName = `${source.firstName} ${source.lastName}`;
target.age = Math.floor(
(Date.now() - source.dateOfBirth.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
})
.build();
🔐 Type-Safe Query Builders
Creating complex query builders with multiple type parameters:
// 🎯 Query builder with table, select, and where types
interface QueryBuilder<
TTable extends Record<string, any>,
TSelect extends keyof TTable = keyof TTable,
TWhere extends Partial<TTable> = Partial<TTable>
> {
select<K extends keyof TTable>(
...fields: K[]
): QueryBuilder<TTable, K, TWhere>;
where<K extends keyof TTable>(
field: K,
value: TTable[K]
): QueryBuilder<TTable, TSelect, TWhere & Record<K, TTable[K]>>;
orderBy<K extends keyof TTable>(
field: K,
direction?: 'asc' | 'desc'
): QueryBuilder<TTable, TSelect, TWhere>;
limit(count: number): QueryBuilder<TTable, TSelect, TWhere>;
execute(): Promise<Pick<TTable, TSelect>[]>;
}
// 🏗️ Advanced query operations
interface AdvancedQueryBuilder<
TTable extends Record<string, any>,
TJoined extends Record<string, any> = {},
TSelect extends keyof (TTable & TJoined) = keyof (TTable & TJoined)
> {
join<
TJoinTable extends Record<string, any>,
TOn extends keyof TTable & keyof TJoinTable
>(
table: string,
on: { left: TOn; right: TOn }
): AdvancedQueryBuilder<TTable, TJoined & TJoinTable, TSelect>;
groupBy<K extends TSelect>(
...fields: K[]
): GroupedQueryBuilder<TTable & TJoined, K>;
having<K extends TSelect>(
field: K,
condition: (value: (TTable & TJoined)[K]) => boolean
): AdvancedQueryBuilder<TTable, TJoined, TSelect>;
}
interface GroupedQueryBuilder<
TTable extends Record<string, any>,
TGroupBy extends keyof TTable
> {
aggregate<TAgg extends Record<string, any>>(
aggregations: {
[K in keyof TAgg]: {
field: keyof TTable;
function: 'sum' | 'avg' | 'count' | 'min' | 'max';
};
}
): Promise<Array<Pick<TTable, TGroupBy> & TAgg>>;
}
// 🔧 Implementation
class QueryBuilderImpl<
TTable extends Record<string, any>,
TSelect extends keyof TTable = keyof TTable,
TWhere extends Partial<TTable> = Partial<TTable>
> implements QueryBuilder<TTable, TSelect, TWhere> {
private selectedFields: TSelect[] = [];
private whereConditions: Partial<TTable> = {};
private orderByField?: keyof TTable;
private orderDirection: 'asc' | 'desc' = 'asc';
private limitCount?: number;
constructor(private tableName: string) {}
select<K extends keyof TTable>(
...fields: K[]
): QueryBuilder<TTable, K, TWhere> {
return new QueryBuilderImpl<TTable, K, TWhere>(this.tableName);
}
where<K extends keyof TTable>(
field: K,
value: TTable[K]
): QueryBuilder<TTable, TSelect, TWhere & Record<K, TTable[K]>> {
this.whereConditions[field] = value;
return this as any;
}
orderBy<K extends keyof TTable>(
field: K,
direction: 'asc' | 'desc' = 'asc'
): QueryBuilder<TTable, TSelect, TWhere> {
this.orderByField = field;
this.orderDirection = direction;
return this;
}
limit(count: number): QueryBuilder<TTable, TSelect, TWhere> {
this.limitCount = count;
return this;
}
async execute(): Promise<Pick<TTable, TSelect>[]> {
// Mock implementation
console.log('Executing query:', {
table: this.tableName,
select: this.selectedFields,
where: this.whereConditions,
orderBy: this.orderByField,
limit: this.limitCount
});
return [];
}
}
// Usage
interface User {
id: number;
name: string;
email: string;
age: number;
isActive: boolean;
createdAt: Date;
}
const query = new QueryBuilderImpl<User>('users')
.select('id', 'name', 'email')
.where('isActive', true)
.where('age', 25)
.orderBy('createdAt', 'desc')
.limit(10);
// Type of result is Pick<User, 'id' | 'name' | 'email'>[]
const users = await query.execute();
🎮 Hands-On Exercise
Let’s build a type-safe event-driven system with multiple type parameters!
📝 Challenge: Advanced Event System
Create an event system that:
- Supports typed events with payloads
- Provides middleware/interceptors
- Includes event replay functionality
- Has typed event handlers with context
// Your challenge: Implement this advanced event system
interface EventConfig<TEvent, TPayload, TContext = {}> {
name: TEvent;
validator?: (payload: TPayload) => boolean;
transformer?: (payload: TPayload, context: TContext) => TPayload;
}
interface EventBus<
TEventMap extends Record<string, any>,
TContext = {}
> {
on<K extends keyof TEventMap>(
event: K,
handler: (payload: TEventMap[K], context: TContext) => void | Promise<void>
): () => void;
emit<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K]
): Promise<void>;
intercept<K extends keyof TEventMap>(
event: K,
interceptor: (
payload: TEventMap[K],
next: () => Promise<void>
) => Promise<void>
): () => void;
replay<K extends keyof TEventMap>(
event: K,
filter?: (payload: TEventMap[K], timestamp: Date) => boolean
): Promise<void>;
}
// Example usage to support:
interface AppEvents {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string; reason?: string };
dataUpdate: { table: string; id: string; changes: any };
error: { code: string; message: string; stack?: string };
}
interface AppContext {
requestId: string;
userId?: string;
permissions: string[];
}
const eventBus = createEventBus<AppEvents, AppContext>();
eventBus.on('userLogin', async (payload, context) => {
console.log(`User ${payload.userId} logged in`);
});
eventBus.intercept('dataUpdate', async (payload, next) => {
console.log('Before update:', payload);
await next();
console.log('After update');
});
💡 Solution
Click to see the solution
// 🎯 Complete advanced event system implementation
interface EventConfig<TEvent, TPayload, TContext = {}> {
name: TEvent;
validator?: (payload: TPayload) => boolean;
transformer?: (payload: TPayload, context: TContext) => TPayload;
metadata?: Record<string, any>;
}
interface EventRecord<TPayload> {
payload: TPayload;
timestamp: Date;
context?: any;
metadata?: Record<string, any>;
}
interface EventBus<
TEventMap extends Record<string, any>,
TContext = {}
> {
on<K extends keyof TEventMap>(
event: K,
handler: (payload: TEventMap[K], context: TContext) => void | Promise<void>
): () => void;
once<K extends keyof TEventMap>(
event: K,
handler: (payload: TEventMap[K], context: TContext) => void | Promise<void>
): () => void;
emit<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context?: TContext
): Promise<void>;
intercept<K extends keyof TEventMap>(
event: K,
interceptor: (
payload: TEventMap[K],
context: TContext,
next: () => Promise<void>
) => Promise<void>
): () => void;
replay<K extends keyof TEventMap>(
event: K,
filter?: (payload: TEventMap[K], timestamp: Date) => boolean
): Promise<void>;
getHistory<K extends keyof TEventMap>(
event?: K
): EventRecord<K extends keyof TEventMap ? TEventMap[K] : any>[];
clear(event?: keyof TEventMap): void;
configure<K extends keyof TEventMap>(
config: EventConfig<K, TEventMap[K], TContext>
): void;
}
// 🏗️ Advanced implementation
class AdvancedEventBus<
TEventMap extends Record<string, any>,
TContext = {}
> implements EventBus<TEventMap, TContext> {
private handlers = new Map<keyof TEventMap, Set<Function>>();
private onceHandlers = new Map<keyof TEventMap, Set<Function>>();
private interceptors = new Map<keyof TEventMap, Array<Function>>();
private history = new Map<keyof TEventMap, EventRecord<any>[]>();
private configs = new Map<keyof TEventMap, EventConfig<any, any, TContext>>();
private defaultContext: TContext;
constructor(defaultContext: TContext) {
this.defaultContext = defaultContext;
}
on<K extends keyof TEventMap>(
event: K,
handler: (payload: TEventMap[K], context: TContext) => void | Promise<void>
): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
return () => {
this.handlers.get(event)?.delete(handler);
};
}
once<K extends keyof TEventMap>(
event: K,
handler: (payload: TEventMap[K], context: TContext) => void | Promise<void>
): () => void {
if (!this.onceHandlers.has(event)) {
this.onceHandlers.set(event, new Set());
}
this.onceHandlers.get(event)!.add(handler);
return () => {
this.onceHandlers.get(event)?.delete(handler);
};
}
async emit<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context?: TContext
): Promise<void> {
const effectiveContext = context || this.defaultContext;
const config = this.configs.get(event);
// Validate payload
if (config?.validator && !config.validator(payload)) {
throw new Error(`Invalid payload for event ${String(event)}`);
}
// Transform payload
let transformedPayload = payload;
if (config?.transformer) {
transformedPayload = config.transformer(payload, effectiveContext);
}
// Record in history
this.recordEvent(event, transformedPayload, effectiveContext);
// Build interceptor chain
const interceptorChain = this.buildInterceptorChain(
event,
transformedPayload,
effectiveContext
);
// Execute with interceptors
await interceptorChain();
}
intercept<K extends keyof TEventMap>(
event: K,
interceptor: (
payload: TEventMap[K],
context: TContext,
next: () => Promise<void>
) => Promise<void>
): () => void {
if (!this.interceptors.has(event)) {
this.interceptors.set(event, []);
}
this.interceptors.get(event)!.push(interceptor);
return () => {
const interceptors = this.interceptors.get(event);
if (interceptors) {
const index = interceptors.indexOf(interceptor);
if (index > -1) {
interceptors.splice(index, 1);
}
}
};
}
async replay<K extends keyof TEventMap>(
event: K,
filter?: (payload: TEventMap[K], timestamp: Date) => boolean
): Promise<void> {
const records = this.history.get(event) || [];
for (const record of records) {
if (!filter || filter(record.payload, record.timestamp)) {
await this.executeHandlers(event, record.payload, record.context);
}
}
}
getHistory<K extends keyof TEventMap>(
event?: K
): EventRecord<K extends keyof TEventMap ? TEventMap[K] : any>[] {
if (event) {
return this.history.get(event) || [];
}
const allHistory: EventRecord<any>[] = [];
this.history.forEach((records) => {
allHistory.push(...records);
});
return allHistory.sort((a, b) =>
a.timestamp.getTime() - b.timestamp.getTime()
);
}
clear(event?: keyof TEventMap): void {
if (event) {
this.handlers.delete(event);
this.onceHandlers.delete(event);
this.interceptors.delete(event);
this.history.delete(event);
this.configs.delete(event);
} else {
this.handlers.clear();
this.onceHandlers.clear();
this.interceptors.clear();
this.history.clear();
this.configs.clear();
}
}
configure<K extends keyof TEventMap>(
config: EventConfig<K, TEventMap[K], TContext>
): void {
this.configs.set(config.name, config);
}
private recordEvent<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context: TContext
): void {
if (!this.history.has(event)) {
this.history.set(event, []);
}
const config = this.configs.get(event);
this.history.get(event)!.push({
payload,
timestamp: new Date(),
context,
metadata: config?.metadata
});
// Limit history size
const maxHistorySize = 1000;
const history = this.history.get(event)!;
if (history.length > maxHistorySize) {
history.splice(0, history.length - maxHistorySize);
}
}
private buildInterceptorChain<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context: TContext
): () => Promise<void> {
const interceptors = this.interceptors.get(event) || [];
const executeCore = async () => {
await this.executeHandlers(event, payload, context);
};
if (interceptors.length === 0) {
return executeCore;
}
// Build chain from interceptors
return interceptors.reduceRight(
(next, interceptor) => {
return async () => {
await interceptor(payload, context, next);
};
},
executeCore
);
}
private async executeHandlers<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context: TContext
): Promise<void> {
// Execute regular handlers
const handlers = this.handlers.get(event);
if (handlers) {
const promises: Promise<void>[] = [];
handlers.forEach(handler => {
const result = handler(payload, context);
if (result instanceof Promise) {
promises.push(result);
}
});
await Promise.all(promises);
}
// Execute once handlers
const onceHandlers = this.onceHandlers.get(event);
if (onceHandlers) {
const handlersToExecute = Array.from(onceHandlers);
onceHandlers.clear();
const promises: Promise<void>[] = [];
handlersToExecute.forEach(handler => {
const result = handler(payload, context);
if (result instanceof Promise) {
promises.push(result);
}
});
await Promise.all(promises);
}
}
}
// 🎨 Factory with middleware support
interface EventBusMiddleware<TEventMap, TContext> {
beforeEmit?<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context: TContext
): void | Promise<void>;
afterEmit?<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context: TContext
): void | Promise<void>;
onError?<K extends keyof TEventMap>(
event: K,
error: Error,
payload: TEventMap[K],
context: TContext
): void | Promise<void>;
}
function createEventBus<
TEventMap extends Record<string, any>,
TContext = {}
>(
defaultContext: TContext,
middleware?: EventBusMiddleware<TEventMap, TContext>
): EventBus<TEventMap, TContext> {
const bus = new AdvancedEventBus<TEventMap, TContext>(defaultContext);
if (middleware) {
// Wrap emit to include middleware
const originalEmit = bus.emit.bind(bus);
bus.emit = async function<K extends keyof TEventMap>(
event: K,
payload: TEventMap[K],
context?: TContext
): Promise<void> {
const effectiveContext = context || defaultContext;
try {
await middleware.beforeEmit?.(event, payload, effectiveContext);
await originalEmit(event, payload, context);
await middleware.afterEmit?.(event, payload, effectiveContext);
} catch (error) {
await middleware.onError?.(
event,
error as Error,
payload,
effectiveContext
);
throw error;
}
};
}
return bus;
}
// 💫 Test the implementation
interface AppEvents {
userLogin: { userId: string; timestamp: Date; ip: string };
userLogout: { userId: string; reason?: string };
dataUpdate: { table: string; id: string; changes: any; user: string };
error: { code: string; message: string; stack?: string; severity: 'low' | 'medium' | 'high' };
performance: { metric: string; value: number; unit: string };
}
interface AppContext {
requestId: string;
userId?: string;
permissions: string[];
timestamp: Date;
}
async function testEventSystem() {
console.log('=== Advanced Event System Test ===\n');
const eventBus = createEventBus<AppEvents, AppContext>(
{
requestId: 'default',
permissions: [],
timestamp: new Date()
},
{
beforeEmit: async (event, payload, context) => {
console.log(`[Middleware] Before ${String(event)}:`, payload);
},
afterEmit: async (event, payload, context) => {
console.log(`[Middleware] After ${String(event)}`);
},
onError: async (event, error, payload, context) => {
console.error(`[Middleware] Error in ${String(event)}:`, error.message);
}
}
);
// Configure events
eventBus.configure({
name: 'userLogin',
validator: (payload) => payload.userId.length > 0,
transformer: (payload, context) => ({
...payload,
timestamp: new Date()
}),
metadata: { critical: true }
});
eventBus.configure({
name: 'error',
validator: (payload) => payload.code.length > 0,
metadata: { logLevel: 'error' }
});
// Set up handlers
eventBus.on('userLogin', async (payload, context) => {
console.log(`🔐 User ${payload.userId} logged in from ${payload.ip}`);
console.log(` Request: ${context.requestId}`);
});
eventBus.once('userLogin', async (payload) => {
console.log(`🎉 First login detected for ${payload.userId}`);
});
const unsubscribe = eventBus.on('dataUpdate', async (payload, context) => {
console.log(`📝 Data update in ${payload.table} by ${payload.user}`);
});
eventBus.on('error', async (payload) => {
const emoji = payload.severity === 'high' ? '🚨' :
payload.severity === 'medium' ? '⚠️' : 'ℹ️';
console.log(`${emoji} Error ${payload.code}: ${payload.message}`);
});
// Set up interceptors
eventBus.intercept('dataUpdate', async (payload, context, next) => {
console.log('🔍 Validating permissions for data update...');
if (!context.permissions.includes('write')) {
throw new Error('Insufficient permissions');
}
console.log('✅ Permissions validated');
await next();
console.log('📊 Logging data update to audit trail');
});
eventBus.intercept('error', async (payload, context, next) => {
if (payload.severity === 'high') {
console.log('🚨 Sending alert for high severity error');
}
await next();
});
// Emit events
const context: AppContext = {
requestId: 'req-123',
userId: 'user-456',
permissions: ['read', 'write'],
timestamp: new Date()
};
console.log('\n--- Emitting Events ---\n');
await eventBus.emit('userLogin', {
userId: 'alice',
timestamp: new Date(),
ip: '192.168.1.1'
}, context);
await eventBus.emit('dataUpdate', {
table: 'users',
id: '123',
changes: { name: 'Alice Smith' },
user: 'alice'
}, context);
await eventBus.emit('error', {
code: 'DB_001',
message: 'Database connection failed',
severity: 'high'
}, context);
await eventBus.emit('performance', {
metric: 'response_time',
value: 125,
unit: 'ms'
}, context);
// Test history and replay
console.log('\n--- Event History ---\n');
const loginHistory = eventBus.getHistory('userLogin');
console.log(`Login events: ${loginHistory.length}`);
console.log('\n--- Replaying Events ---\n');
await eventBus.replay('userLogin');
// Unsubscribe handler
unsubscribe();
// Clear specific event
eventBus.clear('performance');
console.log('\n✅ Test complete!');
}
testEventSystem();
🎯 Summary
You’ve mastered multiple type parameters in TypeScript! 🎉 You learned how to:
- 🧩 Create complex multi-parameter generic types
- 🔗 Build sophisticated type relationships and constraints
- 🎭 Work with higher-order type parameters
- 🔄 Implement advanced patterns like state machines
- 🗺️ Design complex data transformation systems
- ✨ Create professional-grade generic libraries
Multiple type parameters unlock the full power of TypeScript’s type system, enabling you to create incredibly flexible yet type-safe code. You’re now equipped to tackle the most complex generic programming challenges!
Keep building amazing generic systems! 🚀