Prerequisites
- Understanding of class expressions ๐
- Closures and scope knowledge ๐
- Interface and type basics ๐ป
What you'll learn
- Create anonymous classes inline ๐ฏ
- Understand use cases for anonymous classes ๐๏ธ
- Implement factory patterns with anonymous classes ๐ก๏ธ
- Master closure patterns in class contexts โจ
๐ฏ Introduction
Welcome to the mysterious world of anonymous classes! ๐ In this guide, weโll explore how TypeScript allows you to create classes without names, directly at the point where theyโre needed - perfect for one-time-use scenarios.
Youโll discover how anonymous classes are like masked performers ๐ญ - they appear, do their job brilliantly, and disappear without leaving a trace! Whether youโre creating specialized implementations ๐ฏ, building adapter patterns ๐, or designing callback handlers ๐, understanding anonymous classes adds elegance to your code.
By the end of this tutorial, youโll be confidently creating inline classes that capture context and provide exactly what you need, exactly where you need it! Letโs unmask these powerful patterns! ๐โโ๏ธ
๐ Understanding Anonymous Classes
๐ค What are Anonymous Classes?
Anonymous classes are class expressions without a name, created inline at the point of use. Theyโre perfect when you need a class just once and donโt want to pollute your namespace with a named class definition.
Think of anonymous classes like:
- ๐ญ Stand-in actors: Filling a role without needing credits
- ๐ช Pop-up shops: Temporary but fully functional
- ๐จ Sketch vs painting: Quick implementation vs formal definition
- ๐ Shooting stars: Brief but impactful appearances
๐ก When to Use Anonymous Classes
Perfect scenarios for anonymous classes:
- Single-use implementations ๐ฏ: Need a class just once
- Callback handlers ๐: Event-specific implementations
- Test doubles ๐งช: Quick mocks and stubs
- Adapter patterns ๐: Inline interface implementations
Real-world example: Event handlers ๐ฎ - Creating a specialized class just for handling a specific button click!
๐ง Basic Anonymous Classes
๐ Simple Anonymous Class Patterns
Letโs start with fundamental patterns:
// ๐ฏ Basic anonymous class
const myObject = new (class {
private value: number = 0;
increment(): number {
return ++this.value;
}
decrement(): number {
return --this.value;
}
getValue(): number {
return this.value;
}
})();
console.log(myObject.increment()); // 1
console.log(myObject.increment()); // 2
console.log(myObject.getValue()); // 2
// ๐ญ Factory returning anonymous class instance
const createCounter = (initial: number = 0) => {
return new (class {
private count = initial;
next(): number {
return ++this.count;
}
reset(): void {
this.count = initial;
}
toString(): string {
return `Counter(${this.count})`;
}
})();
};
const counter1 = createCounter(10);
const counter2 = createCounter(100);
console.log(counter1.next()); // 11
console.log(counter2.next()); // 101
console.log(counter1.toString()); // "Counter(11)"
// ๐ญ Anonymous class extending base class
abstract class Animal {
abstract makeSound(): string;
describe(): string {
return `This animal says: ${this.makeSound()}`;
}
}
const dog = new (class extends Animal {
makeSound(): string {
return 'Woof!';
}
wagTail(): void {
console.log('*wagging tail happily*');
}
})();
console.log(dog.describe()); // "This animal says: Woof!"
dog.wagTail(); // "*wagging tail happily*"
๐ Anonymous Classes Implementing Interfaces
Using anonymous classes to implement interfaces inline:
// ๐ฏ Interface implementations
interface Logger {
log(message: string): void;
error(message: string): void;
warn(message: string): void;
}
interface LoggerConfig {
prefix?: string;
timestamp?: boolean;
colors?: boolean;
}
// ๐๏ธ Create logger with anonymous class
const createLogger = (config: LoggerConfig = {}): Logger => {
return new (class implements Logger {
private format(level: string, message: string): string {
let output = '';
if (config.timestamp) {
output += `[${new Date().toISOString()}] `;
}
if (config.prefix) {
output += `${config.prefix} `;
}
output += `${level}: ${message}`;
return output;
}
log(message: string): void {
const formatted = this.format('LOG', message);
if (config.colors) {
console.log(`\x1b[37m${formatted}\x1b[0m`); // White
} else {
console.log(formatted);
}
}
error(message: string): void {
const formatted = this.format('ERROR', message);
if (config.colors) {
console.error(`\x1b[31m${formatted}\x1b[0m`); // Red
} else {
console.error(formatted);
}
}
warn(message: string): void {
const formatted = this.format('WARN', message);
if (config.colors) {
console.warn(`\x1b[33m${formatted}\x1b[0m`); // Yellow
} else {
console.warn(formatted);
}
}
})();
};
// ๐ซ Different logger configurations
const simpleLogger = createLogger();
const fancyLogger = createLogger({
prefix: '[MyApp]',
timestamp: true,
colors: true
});
simpleLogger.log('Hello world');
fancyLogger.error('Something went wrong!');
๐ Advanced Anonymous Class Patterns
๐จ Closure-Based Anonymous Classes
Leveraging closures for private state:
// ๐ State machine with anonymous class
type State = 'idle' | 'loading' | 'success' | 'error';
type StateHandler = () => void;
const createStateMachine = (initialState: State) => {
// Private state via closure
let currentState = initialState;
const handlers = new Map<State, StateHandler[]>();
const history: State[] = [initialState];
return new (class {
getState(): State {
return currentState;
}
setState(newState: State): void {
if (currentState !== newState) {
history.push(newState);
currentState = newState;
this.notifyHandlers();
}
}
onStateChange(state: State, handler: StateHandler): () => void {
if (!handlers.has(state)) {
handlers.set(state, []);
}
handlers.get(state)!.push(handler);
// Return unsubscribe function
return () => {
const stateHandlers = handlers.get(state);
if (stateHandlers) {
const index = stateHandlers.indexOf(handler);
if (index > -1) {
stateHandlers.splice(index, 1);
}
}
};
}
private notifyHandlers(): void {
const stateHandlers = handlers.get(currentState);
if (stateHandlers) {
stateHandlers.forEach(handler => handler());
}
}
getHistory(): State[] {
return [...history];
}
canTransition(from: State, to: State): boolean {
const validTransitions: Record<State, State[]> = {
idle: ['loading'],
loading: ['success', 'error'],
success: ['idle', 'loading'],
error: ['idle', 'loading']
};
return validTransitions[from]?.includes(to) ?? false;
}
transition(to: State): boolean {
if (this.canTransition(currentState, to)) {
this.setState(to);
return true;
}
return false;
}
})();
};
// ๐ซ Usage
const machine = createStateMachine('idle');
machine.onStateChange('loading', () => {
console.log('๐ Loading started...');
});
machine.onStateChange('success', () => {
console.log('โ
Operation successful!');
});
machine.onStateChange('error', () => {
console.log('โ Operation failed!');
});
console.log(machine.transition('loading')); // true - "๐ Loading started..."
console.log(machine.transition('success')); // true - "โ
Operation successful!"
console.log(machine.getHistory()); // ['idle', 'loading', 'success']
๐๏ธ Builder Pattern with Anonymous Classes
Creating fluent builders inline:
// ๐ฏ Generic builder interface
interface Builder<T> {
build(): T;
}
// ๐ House building example
interface House {
floors: number;
rooms: number;
garage: boolean;
pool: boolean;
garden: boolean;
style: 'modern' | 'traditional' | 'minimalist';
}
const createHouseBuilder = () => {
// Private state
const house: Partial<House> = {
floors: 1,
rooms: 1,
garage: false,
pool: false,
garden: false,
style: 'traditional'
};
return new (class implements Builder<House> {
withFloors(floors: number) {
house.floors = floors;
return this;
}
withRooms(rooms: number) {
house.rooms = rooms;
return this;
}
withGarage() {
house.garage = true;
return this;
}
withPool() {
house.pool = true;
return this;
}
withGarden() {
house.garden = true;
return this;
}
withStyle(style: House['style']) {
house.style = style;
return this;
}
build(): House {
// Validation
if (house.rooms! < house.floors!) {
throw new Error('Must have at least one room per floor');
}
if (house.pool && house.floors! > 3) {
throw new Error('Pool not recommended for tall buildings');
}
return { ...house } as House;
}
})();
};
// ๐ซ Fluent API usage
const mansion = createHouseBuilder()
.withFloors(3)
.withRooms(10)
.withGarage()
.withPool()
.withGarden()
.withStyle('modern')
.build();
console.log(mansion);
// { floors: 3, rooms: 10, garage: true, pool: true, garden: true, style: 'modern' }
const cottage = createHouseBuilder()
.withFloors(1)
.withRooms(3)
.withGarden()
.withStyle('traditional')
.build();
console.log(cottage);
// { floors: 1, rooms: 3, garage: false, pool: false, garden: true, style: 'traditional' }
๐ช Event System with Anonymous Classes
๐ก Type-Safe Event Emitters
Building event systems with anonymous classes:
// ๐ฏ Typed event system
type EventMap = Record<string, any[]>;
interface Emitter<T extends EventMap> {
on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void;
off<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void;
emit<K extends keyof T>(event: K, ...args: T[K]): void;
once<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void;
}
// ๐๏ธ Create typed event emitter
const createEmitter = <T extends EventMap>(): Emitter<T> => {
const events = new Map<keyof T, Set<Function>>();
return new (class implements Emitter<T> {
on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void {
if (!events.has(event)) {
events.set(event, new Set());
}
events.get(event)!.add(handler);
}
off<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void {
events.get(event)?.delete(handler);
}
emit<K extends keyof T>(event: K, ...args: T[K]): void {
events.get(event)?.forEach(handler => {
handler(...args);
});
}
once<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void {
const wrappedHandler = (...args: T[K]) => {
handler(...args);
this.off(event, wrappedHandler);
};
this.on(event, wrappedHandler);
}
})();
};
// ๐ซ Type-safe usage
interface GameEvents {
start: [];
pause: [];
resume: [];
score: [points: number, player: string];
gameOver: [winner: string, finalScore: number];
powerUp: [type: 'speed' | 'shield' | 'damage', duration: number];
}
const gameEmitter = createEmitter<GameEvents>();
gameEmitter.on('score', (points, player) => {
console.log(`${player} scored ${points} points!`);
});
gameEmitter.on('powerUp', (type, duration) => {
console.log(`Power-up activated: ${type} for ${duration} seconds`);
});
gameEmitter.once('gameOver', (winner, score) => {
console.log(`๐ Game Over! ${winner} wins with ${score} points!`);
});
// Emit events
gameEmitter.emit('start');
gameEmitter.emit('score', 100, 'Player1');
gameEmitter.emit('powerUp', 'speed', 10);
gameEmitter.emit('gameOver', 'Player1', 500);
๐ Observable Pattern with Anonymous Classes
Creating reactive data structures:
// ๐ฏ Observable value wrapper
interface Observer<T> {
next(value: T): void;
error?(error: Error): void;
complete?(): void;
}
interface Observable<T> {
subscribe(observer: Observer<T>): () => void;
getValue(): T;
}
const createObservable = <T>(initialValue: T): Observable<T> & { setValue(value: T): void } => {
let currentValue = initialValue;
const observers = new Set<Observer<T>>();
return new (class {
getValue(): T {
return currentValue;
}
setValue(value: T): void {
currentValue = value;
this.notify(value);
}
subscribe(observer: Observer<T>): () => void {
observers.add(observer);
observer.next(currentValue); // Emit current value immediately
return () => {
observers.delete(observer);
};
}
private notify(value: T): void {
observers.forEach(observer => {
try {
observer.next(value);
} catch (error) {
observer.error?.(error as Error);
}
});
}
})();
};
// ๐๏ธ Computed observables
const createComputed = <T, R>(
source: Observable<T>,
transform: (value: T) => R
): Observable<R> => {
let cachedValue = transform(source.getValue());
const observers = new Set<Observer<R>>();
// Subscribe to source
source.subscribe({
next(value: T) {
cachedValue = transform(value);
observers.forEach(observer => observer.next(cachedValue));
}
});
return new (class {
getValue(): R {
return cachedValue;
}
subscribe(observer: Observer<R>): () => void {
observers.add(observer);
observer.next(cachedValue);
return () => {
observers.delete(observer);
};
}
})();
};
// ๐ซ Reactive system
const temperature = createObservable(20); // Celsius
const fahrenheit = createComputed(temperature, c => (c * 9/5) + 32);
const status = createComputed(temperature, t =>
t < 0 ? 'freezing' : t < 10 ? 'cold' : t < 25 ? 'comfortable' : 'hot'
);
// Subscribe to changes
fahrenheit.subscribe({
next(value) {
console.log(`Temperature: ${value}ยฐF`);
}
});
status.subscribe({
next(value) {
console.log(`Status: ${value}`);
}
});
// Update temperature
temperature.setValue(30); // "Temperature: 86ยฐF", "Status: hot"
temperature.setValue(-5); // "Temperature: 23ยฐF", "Status: freezing"
๐ญ Testing with Anonymous Classes
๐งช Mock Objects and Test Doubles
Creating test doubles inline:
// ๐ฏ Testing interfaces
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<boolean>;
}
interface User {
id: string;
name: string;
email: string;
}
interface EmailService {
sendWelcomeEmail(user: User): Promise<void>;
sendPasswordReset(email: string): Promise<void>;
}
// ๐๏ธ Service under test
class UserService {
constructor(
private repo: UserRepository,
private emailService: EmailService
) {}
async createUser(name: string, email: string): Promise<User> {
const user: User = {
id: Date.now().toString(),
name,
email
};
await this.repo.save(user);
await this.emailService.sendWelcomeEmail(user);
return user;
}
async deleteUser(id: string): Promise<boolean> {
const user = await this.repo.findById(id);
if (!user) return false;
return this.repo.delete(id);
}
}
// ๐งช Test with anonymous mocks
const testUserService = () => {
// Track calls for assertions
const calls = {
save: [] as User[],
sendWelcomeEmail: [] as User[],
findById: [] as string[],
delete: [] as string[]
};
// Create mocks with anonymous classes
const mockRepo = new (class implements UserRepository {
async findById(id: string): Promise<User | null> {
calls.findById.push(id);
return id === '123'
? { id: '123', name: 'Test User', email: '[email protected]' }
: null;
}
async save(user: User): Promise<void> {
calls.save.push(user);
}
async delete(id: string): Promise<boolean> {
calls.delete.push(id);
return id === '123';
}
})();
const mockEmailService = new (class implements EmailService {
async sendWelcomeEmail(user: User): Promise<void> {
calls.sendWelcomeEmail.push(user);
}
async sendPasswordReset(email: string): Promise<void> {
// Not used in this test
}
})();
// Run tests
const service = new UserService(mockRepo, mockEmailService);
// Test create user
service.createUser('Alice', '[email protected]').then(user => {
console.assert(calls.save.length === 1, 'Save should be called once');
console.assert(calls.sendWelcomeEmail.length === 1, 'Email should be sent');
console.assert(user.name === 'Alice', 'User name should match');
console.log('โ
Create user test passed');
});
// Test delete user
service.deleteUser('123').then(result => {
console.assert(result === true, 'Should delete existing user');
console.assert(calls.findById.includes('123'), 'Should look up user');
console.assert(calls.delete.includes('123'), 'Should call delete');
console.log('โ
Delete user test passed');
});
service.deleteUser('999').then(result => {
console.assert(result === false, 'Should not delete non-existent user');
console.log('โ
Delete non-existent user test passed');
});
};
testUserService();
๐ก๏ธ Adapter Pattern with Anonymous Classes
๐ Creating Adapters Inline
Adapting interfaces on the fly:
// ๐ฏ Legacy and modern interfaces
interface LegacyPaymentGateway {
makePayment(amount: number, currency: string, cardNumber: string): string;
checkStatus(transactionId: string): 'pending' | 'completed' | 'failed';
}
interface ModernPaymentGateway {
charge(params: {
amount: number;
currency: string;
source: string;
metadata?: Record<string, any>;
}): Promise<{ id: string; status: string }>;
getTransaction(id: string): Promise<{
id: string;
status: 'pending' | 'succeeded' | 'failed';
amount: number;
}>;
}
// ๐๏ธ Adapt legacy to modern interface
const adaptLegacyGateway = (legacy: LegacyPaymentGateway): ModernPaymentGateway => {
return new (class implements ModernPaymentGateway {
async charge(params: {
amount: number;
currency: string;
source: string;
metadata?: Record<string, any>;
}): Promise<{ id: string; status: string }> {
// Adapt the call
const transactionId = legacy.makePayment(
params.amount,
params.currency,
params.source
);
// Simulate async behavior
await new Promise(resolve => setTimeout(resolve, 100));
const legacyStatus = legacy.checkStatus(transactionId);
const modernStatus = this.convertStatus(legacyStatus);
return { id: transactionId, status: modernStatus };
}
async getTransaction(id: string): Promise<{
id: string;
status: 'pending' | 'succeeded' | 'failed';
amount: number;
}> {
const legacyStatus = legacy.checkStatus(id);
return {
id,
status: legacyStatus === 'completed' ? 'succeeded' : legacyStatus === 'failed' ? 'failed' : 'pending',
amount: 0 // Legacy doesn't provide this
};
}
private convertStatus(legacyStatus: string): string {
switch (legacyStatus) {
case 'completed': return 'succeeded';
case 'failed': return 'failed';
default: return 'pending';
}
}
})();
};
// ๐ซ Usage
const legacyGateway: LegacyPaymentGateway = {
makePayment(amount, currency, cardNumber) {
console.log(`Legacy payment: ${amount} ${currency}`);
return `TXN_${Date.now()}`;
},
checkStatus(transactionId) {
// Simulate random status
const statuses: ('pending' | 'completed' | 'failed')[] = ['pending', 'completed', 'failed'];
return statuses[Math.floor(Math.random() * statuses.length)];
}
};
const modernGateway = adaptLegacyGateway(legacyGateway);
// Use modern interface
modernGateway.charge({
amount: 100,
currency: 'USD',
source: '4242424242424242',
metadata: { orderId: '12345' }
}).then(result => {
console.log('Payment result:', result);
});
๐ฎ Hands-On Exercise
Letโs build a plugin system using anonymous classes!
๐ Challenge: Dynamic Plugin System
Create a plugin system that:
- Allows plugins to be defined inline as anonymous classes
- Supports plugin dependencies
- Provides lifecycle hooks
- Maintains type safety
// Your challenge: Implement this plugin system
interface Plugin {
name: string;
version: string;
dependencies?: string[];
install(app: Application): void;
uninstall?(app: Application): void;
}
interface Application {
plugins: Map<string, Plugin>;
state: Map<string, any>;
use(plugin: Plugin): this;
has(pluginName: string): boolean;
get<T>(key: string): T | undefined;
set(key: string, value: any): void;
}
// Example usage to implement:
const app = createApplication();
// Define plugins inline
app.use(new (class implements Plugin {
name = 'logger';
version = '1.0.0';
install(app: Application): void {
app.set('log', (message: string) => {
console.log(`[${new Date().toISOString()}] ${message}`);
});
}
})());
app.use(new (class implements Plugin {
name = 'router';
version = '1.0.0';
dependencies = ['logger'];
install(app: Application): void {
const log = app.get<(msg: string) => void>('log');
const routes = new Map<string, Function>();
app.set('route', (path: string, handler: Function) => {
routes.set(path, handler);
log?.(`Route registered: ${path}`);
});
app.set('navigate', (path: string) => {
const handler = routes.get(path);
if (handler) {
log?.(`Navigating to: ${path}`);
handler();
}
});
}
})());
// Implement the createApplication function!
๐ก Solution
Click to see the solution
const createApplication = (): Application => {
const plugins = new Map<string, Plugin>();
const state = new Map<string, any>();
const loadOrder: string[] = [];
return new (class implements Application {
plugins = plugins;
state = state;
use(plugin: Plugin): this {
// Check if already installed
if (this.has(plugin.name)) {
console.warn(`Plugin ${plugin.name} is already installed`);
return this;
}
// Check dependencies
if (plugin.dependencies) {
for (const dep of plugin.dependencies) {
if (!this.has(dep)) {
throw new Error(`Plugin ${plugin.name} requires ${dep} to be installed first`);
}
}
}
// Install plugin
console.log(`Installing plugin: ${plugin.name} v${plugin.version}`);
plugin.install(this);
plugins.set(plugin.name, plugin);
loadOrder.push(plugin.name);
return this;
}
has(pluginName: string): boolean {
return plugins.has(pluginName);
}
get<T>(key: string): T | undefined {
return state.get(key) as T;
}
set(key: string, value: any): void {
state.set(key, value);
}
// Additional methods
uninstall(pluginName: string): boolean {
const plugin = plugins.get(pluginName);
if (!plugin) return false;
// Check if other plugins depend on this
const dependents = Array.from(plugins.values()).filter(p =>
p.dependencies?.includes(pluginName)
);
if (dependents.length > 0) {
throw new Error(`Cannot uninstall ${pluginName}: required by ${dependents.map(p => p.name).join(', ')}`);
}
// Uninstall
plugin.uninstall?.(this);
plugins.delete(pluginName);
// Remove from load order
const index = loadOrder.indexOf(pluginName);
if (index > -1) {
loadOrder.splice(index, 1);
}
return true;
}
getLoadOrder(): string[] {
return [...loadOrder];
}
reset(): void {
// Uninstall in reverse order
[...loadOrder].reverse().forEach(name => {
const plugin = plugins.get(name);
plugin?.uninstall?.(this);
});
plugins.clear();
state.clear();
loadOrder.length = 0;
}
})();
};
// ๐ฎ Extended example with more plugins
const app = createApplication();
// Logger plugin
app.use(new (class implements Plugin {
name = 'logger';
version = '1.0.0';
install(app: Application): void {
const logs: string[] = [];
app.set('log', (message: string) => {
const entry = `[${new Date().toISOString()}] ${message}`;
logs.push(entry);
console.log(entry);
});
app.set('getLogs', () => [...logs]);
app.set('clearLogs', () => logs.length = 0);
}
uninstall(app: Application): void {
app.set('log', undefined);
app.set('getLogs', undefined);
app.set('clearLogs', undefined);
}
})());
// Auth plugin
app.use(new (class implements Plugin {
name = 'auth';
version = '1.0.0';
dependencies = ['logger'];
install(app: Application): void {
const log = app.get<(msg: string) => void>('log')!;
let currentUser: { id: string; name: string } | null = null;
app.set('login', (id: string, name: string) => {
currentUser = { id, name };
log(`User logged in: ${name}`);
});
app.set('logout', () => {
if (currentUser) {
log(`User logged out: ${currentUser.name}`);
currentUser = null;
}
});
app.set('getCurrentUser', () => currentUser);
app.set('isAuthenticated', () => currentUser !== null);
}
})());
// API plugin
app.use(new (class implements Plugin {
name = 'api';
version = '1.0.0';
dependencies = ['logger', 'auth'];
install(app: Application): void {
const log = app.get<(msg: string) => void>('log')!;
const isAuthenticated = app.get<() => boolean>('isAuthenticated')!;
app.set('apiRequest', async (endpoint: string, options?: RequestInit) => {
if (!isAuthenticated()) {
throw new Error('Must be authenticated to make API requests');
}
log(`API request: ${options?.method || 'GET'} ${endpoint}`);
// Simulate API call
return { success: true, data: { endpoint } };
});
}
})());
// Use the plugins
const log = app.get<(msg: string) => void>('log')!;
const login = app.get<(id: string, name: string) => void>('login')!;
const apiRequest = app.get<(endpoint: string) => Promise<any>>('apiRequest')!;
log('Application started');
login('123', 'Alice');
apiRequest('/users').then(result => {
log(`API response: ${JSON.stringify(result)}`);
});
console.log('Load order:', app.getLoadOrder());
// ['logger', 'auth', 'api']
๐ฏ Summary
Youโve mastered anonymous classes in TypeScript! ๐ You learned how to:
- ๐ญ Create inline class definitions without names
- ๐ Leverage closures for private state
- ๐๏ธ Build factory patterns with anonymous classes
- ๐งช Create test doubles and mocks inline
- ๐ Implement adapter patterns on the fly
- โจ Design flexible plugin systems
Anonymous classes provide elegant solutions for one-time-use scenarios, keeping your code clean and focused without namespace pollution!