Prerequisites
- Understanding of classes in TypeScript ๐
- Function expressions knowledge ๐
- Generic types understanding ๐ป
What you'll learn
- Create classes dynamically with expressions ๐ฏ
- Build factory functions that return classes ๐๏ธ
- Implement conditional class creation ๐ก๏ธ
- Design flexible class hierarchies โจ
๐ฏ Introduction
Welcome to the dynamic world of class expressions! ๐ In this guide, weโll explore how TypeScript allows you to create classes as expressions rather than declarations, enabling powerful runtime class creation patterns.
Youโll discover how class expressions are like artistโs palettes ๐จ - you can mix and create new classes on the fly! Whether youโre building factory patterns ๐ญ, creating conditional classes ๐, or designing plugin systems ๐, understanding class expressions opens up flexible architectural possibilities.
By the end of this tutorial, youโll be confidently creating classes dynamically and building sophisticated class factories! Letโs paint with classes! ๐โโ๏ธ
๐ Understanding Class Expressions
๐ค What are Class Expressions?
Class expressions are a way to define classes as values rather than declarations. Just like function expressions, they can be assigned to variables, passed as arguments, or returned from functions - giving you runtime control over class creation!
Think of class expressions like:
- ๐จ Paint mixing: Creating new colors (classes) on demand
- ๐ญ Factory assembly: Building products (classes) to specification
- ๐งฌ DNA splicing: Combining traits dynamically
- ๐ญ Costume changes: Different appearances for different scenes
๐ก Class Declaration vs Expression
Hereโs the key difference:
// ๐ Class Declaration (hoisted, named)
class MyClass {
// Class body
}
// ๐จ Class Expression (not hoisted, can be anonymous)
const MyClass = class {
// Class body
};
// ๐ท๏ธ Named Class Expression
const MyClass = class InternalName {
// Can reference InternalName inside class
};
Real-world example: React components ๐ฏ - Higher-order components often use class expressions to create wrapper classes dynamically!
๐ง Basic Class Expressions
๐ Anonymous Class Expressions
Letโs start with the fundamentals:
// ๐ฏ Simple anonymous class expression
const Animal = class {
constructor(public name: string) {}
speak(): string {
return `${this.name} makes a sound`;
}
};
const dog = new Animal('Buddy');
console.log(dog.speak()); // "Buddy makes a sound"
// ๐ญ Factory function returning class
const createClass = (baseValue: number) => {
return class {
value = baseValue;
increment(): number {
return ++this.value;
}
decrement(): number {
return --this.value;
}
};
};
const CounterClass = createClass(10);
const counter = new CounterClass();
console.log(counter.increment()); // 11
console.log(counter.value); // 11
// ๐จ Conditional class creation
const createLogger = (useConsole: boolean) => {
if (useConsole) {
return class ConsoleLogger {
log(message: string): void {
console.log(`[Console]: ${message}`);
}
};
} else {
return class FileLogger {
private logs: string[] = [];
log(message: string): void {
this.logs.push(`[File]: ${message}`);
}
getLogs(): string[] {
return this.logs;
}
};
}
};
const Logger = createLogger(false);
const logger = new Logger();
logger.log('Hello');
// Type-safe access to FileLogger methods
if ('getLogs' in logger) {
console.log(logger.getLogs());
}
๐ท๏ธ Named Class Expressions
Named expressions provide internal references:
// ๐ Self-referencing class expression
const Fibonacci = class Fib {
static cache = new Map<number, number>();
static calculate(n: number): number {
if (n <= 1) return n;
if (Fib.cache.has(n)) {
return Fib.cache.get(n)!;
}
const result = Fib.calculate(n - 1) + Fib.calculate(n - 2);
Fib.cache.set(n, result);
return result;
}
static reset(): void {
Fib.cache.clear();
}
};
console.log(Fibonacci.calculate(10)); // 55
console.log(Fibonacci.cache.size); // 9 cached values
// ๐ญ Class with internal name for debugging
const Component = class MyComponent {
static instances = 0;
id: number;
constructor() {
this.id = ++MyComponent.instances;
console.log(`Creating ${MyComponent.name} #${this.id}`);
}
destroy(): void {
console.log(`Destroying ${MyComponent.name} #${this.id}`);
}
};
const comp1 = new Component(); // "Creating MyComponent #1"
const comp2 = new Component(); // "Creating MyComponent #2"
๐ Advanced Patterns
๐ญ Generic Class Factories
Creating flexible class factories with generics:
// ๐งฌ Generic class factory
const createRepository = <T extends { id: string }>() => {
return class Repository {
private items = new Map<string, T>();
add(item: T): void {
this.items.set(item.id, item);
}
get(id: string): T | undefined {
return this.items.get(id);
}
update(id: string, updates: Partial<T>): T | undefined {
const item = this.items.get(id);
if (item) {
const updated = { ...item, ...updates };
this.items.set(id, updated);
return updated;
}
return undefined;
}
delete(id: string): boolean {
return this.items.delete(id);
}
findAll(): T[] {
return Array.from(this.items.values());
}
findBy(predicate: (item: T) => boolean): T[] {
return this.findAll().filter(predicate);
}
};
};
// ๐ผ Type-safe repositories
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
const UserRepository = createRepository<User>();
const ProductRepository = createRepository<Product>();
const userRepo = new UserRepository();
userRepo.add({ id: '1', name: 'Alice', email: '[email protected]', role: 'admin' });
userRepo.add({ id: '2', name: 'Bob', email: '[email protected]', role: 'user' });
const admins = userRepo.findBy(user => user.role === 'admin');
console.log(admins); // [{ id: '1', name: 'Alice', ... }]
const productRepo = new ProductRepository();
productRepo.add({ id: 'p1', name: 'Laptop', price: 999, stock: 10 });
productRepo.update('p1', { stock: 8 });
๐จ Dynamic Class Extension
Creating classes that extend dynamically:
// ๐ง Mixin-style class factory
type Constructor<T = {}> = new (...args: any[]) => T;
const createEnhancedClass = <TBase extends Constructor>(Base: TBase) => {
return class Enhanced extends Base {
private _metadata = new Map<string, any>();
private _observers = new Set<(key: string, value: any) => void>();
setMetadata(key: string, value: any): void {
this._metadata.set(key, value);
this._notifyObservers(key, value);
}
getMetadata(key: string): any {
return this._metadata.get(key);
}
observe(callback: (key: string, value: any) => void): () => void {
this._observers.add(callback);
return () => this._observers.delete(callback);
}
private _notifyObservers(key: string, value: any): void {
this._observers.forEach(callback => callback(key, value));
}
toJSON(): any {
const baseJSON = 'toJSON' in super.prototype
? (super as any).toJSON.call(this)
: { ...this };
return {
...baseJSON,
_metadata: Object.fromEntries(this._metadata)
};
}
};
};
// ๐๏ธ Base classes
class Person {
constructor(public name: string, public age: number) {}
}
class Vehicle {
constructor(public brand: string, public model: string) {}
getInfo(): string {
return `${this.brand} ${this.model}`;
}
}
// ๐ฏ Enhanced versions
const EnhancedPerson = createEnhancedClass(Person);
const EnhancedVehicle = createEnhancedClass(Vehicle);
const person = new EnhancedPerson('Alice', 30);
person.setMetadata('department', 'Engineering');
person.setMetadata('level', 'Senior');
const unsubscribe = person.observe((key, value) => {
console.log(`Metadata changed: ${key} = ${value}`);
});
person.setMetadata('level', 'Lead'); // "Metadata changed: level = Lead"
const vehicle = new EnhancedVehicle('Tesla', 'Model 3');
vehicle.setMetadata('color', 'red');
vehicle.setMetadata('autopilot', true);
console.log(vehicle.getInfo()); // "Tesla Model 3"
console.log(vehicle.toJSON()); // Includes metadata
๐ญ Conditional Class Creation
๐ Runtime Class Selection
Creating different classes based on conditions:
// ๐ฏ Environment-based class creation
type Environment = 'development' | 'production' | 'test';
const createDatabase = (env: Environment) => {
switch (env) {
case 'development':
return class DevelopmentDB {
private data = new Map<string, any>();
async connect(): Promise<void> {
console.log('๐ Connected to in-memory database');
}
async query(sql: string): Promise<any[]> {
console.log(`๐ Executing: ${sql}`);
// Simulate query with in-memory data
return Array.from(this.data.values());
}
async insert(table: string, data: any): Promise<void> {
const id = Date.now().toString();
this.data.set(id, { id, table, ...data });
console.log(`โ Inserted into ${table}:`, data);
}
reset(): void {
this.data.clear();
console.log('๐ Database reset');
}
};
case 'production':
return class ProductionDB {
private connection: any;
async connect(): Promise<void> {
// Real database connection
console.log('๐ Connected to production database');
}
async query(sql: string): Promise<any[]> {
// Real query execution
console.log(`โก Executing optimized query`);
return [];
}
async insert(table: string, data: any): Promise<void> {
// Real insert with transactions
console.log(`๐พ Inserting into ${table} with transaction`);
}
async backup(): Promise<void> {
console.log('๐ผ Creating database backup');
}
};
case 'test':
return class TestDB {
private mocks = new Map<string, any[]>();
async connect(): Promise<void> {
console.log('๐งช Connected to test database');
}
async query(sql: string): Promise<any[]> {
// Return mocked data
return this.mocks.get(sql) || [];
}
async insert(table: string, data: any): Promise<void> {
// Store in mocks
const existing = this.mocks.get(table) || [];
this.mocks.set(table, [...existing, data]);
}
mock(key: string, data: any[]): void {
this.mocks.set(key, data);
}
verify(): Map<string, any[]> {
return new Map(this.mocks);
}
};
}
};
// ๐ซ Usage based on environment
const Database = createDatabase(process.env.NODE_ENV as Environment || 'development');
const db = new Database();
await db.connect();
await db.insert('users', { name: 'Alice', email: '[email protected]' });
// Type-safe environment-specific methods
if ('reset' in db) {
// Development environment
db.reset();
} else if ('backup' in db) {
// Production environment
await db.backup();
} else if ('mock' in db) {
// Test environment
db.mock('SELECT * FROM users', [{ id: 1, name: 'Test User' }]);
}
๐๏ธ Feature-Based Class Composition
Building classes with optional features:
// ๐ Feature flags type
interface Features {
logging?: boolean;
caching?: boolean;
validation?: boolean;
metrics?: boolean;
}
// ๐จ Dynamic API client factory
const createAPIClient = (features: Features = {}) => {
// Base class
let ClientClass = class {
constructor(protected baseURL: string) {}
protected async request(endpoint: string, options: RequestInit = {}): Promise<any> {
const response = await fetch(`${this.baseURL}${endpoint}`, options);
return response.json();
}
};
// Add logging
if (features.logging) {
const BaseClass = ClientClass;
ClientClass = class extends BaseClass {
protected async request(endpoint: string, options: RequestInit = {}): Promise<any> {
console.log(`๐ก API Request: ${options.method || 'GET'} ${endpoint}`);
const start = Date.now();
try {
const result = await super.request(endpoint, options);
console.log(`โ
Response received in ${Date.now() - start}ms`);
return result;
} catch (error) {
console.error(`โ Request failed:`, error);
throw error;
}
}
};
}
// Add caching
if (features.caching) {
const BaseClass = ClientClass;
ClientClass = class extends BaseClass {
private cache = new Map<string, { data: any; timestamp: number }>();
private cacheTTL = 5 * 60 * 1000; // 5 minutes
protected async request(endpoint: string, options: RequestInit = {}): Promise<any> {
if (options.method === 'GET' || !options.method) {
const cacheKey = endpoint;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
console.log(`๐พ Cache hit: ${endpoint}`);
return cached.data;
}
}
const result = await super.request(endpoint, options);
if (options.method === 'GET' || !options.method) {
this.cache.set(endpoint, { data: result, timestamp: Date.now() });
}
return result;
}
clearCache(): void {
this.cache.clear();
}
};
}
// Add validation
if (features.validation) {
const BaseClass = ClientClass;
ClientClass = class extends BaseClass {
private validators = new Map<string, (data: any) => boolean>();
addValidator(endpoint: string, validator: (data: any) => boolean): void {
this.validators.set(endpoint, validator);
}
protected async request(endpoint: string, options: RequestInit = {}): Promise<any> {
const result = await super.request(endpoint, options);
const validator = this.validators.get(endpoint);
if (validator && !validator(result)) {
throw new Error(`Validation failed for ${endpoint}`);
}
return result;
}
};
}
// Add metrics
if (features.metrics) {
const BaseClass = ClientClass;
ClientClass = class extends BaseClass {
private metrics = {
requests: 0,
errors: 0,
totalTime: 0,
endpoints: new Map<string, number>()
};
protected async request(endpoint: string, options: RequestInit = {}): Promise<any> {
this.metrics.requests++;
const count = this.metrics.endpoints.get(endpoint) || 0;
this.metrics.endpoints.set(endpoint, count + 1);
const start = Date.now();
try {
const result = await super.request(endpoint, options);
this.metrics.totalTime += Date.now() - start;
return result;
} catch (error) {
this.metrics.errors++;
throw error;
}
}
getMetrics() {
return {
...this.metrics,
averageTime: this.metrics.totalTime / this.metrics.requests,
errorRate: this.metrics.errors / this.metrics.requests,
endpoints: Object.fromEntries(this.metrics.endpoints)
};
}
};
}
return ClientClass;
};
// ๐ซ Create different API clients
const BasicClient = createAPIClient();
const basicAPI = new BasicClient('https://api.example.com');
const FullClient = createAPIClient({
logging: true,
caching: true,
validation: true,
metrics: true
});
const fullAPI = new FullClient('https://api.example.com');
// Type-safe feature access
if ('addValidator' in fullAPI) {
fullAPI.addValidator('/users', (data) => Array.isArray(data));
}
if ('getMetrics' in fullAPI) {
console.log(fullAPI.getMetrics());
}
๐ช Class Expression Patterns
๐ Self-Modifying Classes
Classes that modify themselves:
// ๐งฌ Evolving class pattern
const createEvolvingClass = () => {
let version = 1;
return class EvolvingEntity {
static version = version;
private features: string[] = [];
constructor(public name: string) {
this.features.push(`v${EvolvingEntity.version}`);
}
static evolve(newFeature: string): typeof EvolvingEntity {
version++;
const CurrentClass = this;
return class extends CurrentClass {
static version = version;
constructor(name: string) {
super(name);
this.features.push(newFeature);
}
[newFeature](): void {
console.log(`${this.name} uses ${newFeature}!`);
}
} as any;
}
describe(): string {
return `${this.name} has features: ${this.features.join(', ')}`;
}
};
};
// ๐ฎ Evolution in action
let Entity = createEvolvingClass();
const basic = new Entity('Basic');
console.log(basic.describe()); // "Basic has features: v1"
Entity = Entity.evolve('fly');
const flying = new Entity('Flyer');
console.log(flying.describe()); // "Flyer has features: v1, fly"
(flying as any).fly(); // "Flyer uses fly!"
Entity = Entity.evolve('swim');
const swimming = new Entity('Swimmer');
console.log(swimming.describe()); // "Swimmer has features: v1, fly, swim"
(swimming as any).fly(); // "Swimmer uses fly!"
(swimming as any).swim(); // "Swimmer uses swim!"
๐ท๏ธ Tagged Class Templates
Creating specialized classes with metadata:
// ๐ท๏ธ Class tagging system
interface ClassMetadata {
name: string;
version: string;
author: string;
tags: string[];
}
const createTaggedClass = <T extends object>(
metadata: ClassMetadata,
implementation: T
) => {
const TaggedClass = class {
static metadata = metadata;
static hasTag(tag: string): boolean {
return TaggedClass.metadata.tags.includes(tag);
}
static addTag(tag: string): void {
if (!TaggedClass.metadata.tags.includes(tag)) {
TaggedClass.metadata.tags.push(tag);
}
}
static getInfo(): string {
const { name, version, author, tags } = TaggedClass.metadata;
return `${name} v${version} by ${author} [${tags.join(', ')}]`;
}
};
// Merge implementation
Object.assign(TaggedClass.prototype, implementation);
return TaggedClass as typeof TaggedClass & (new () => T);
};
// ๐ฏ Create tagged classes
const HTTPClient = createTaggedClass(
{
name: 'HTTPClient',
version: '2.0.0',
author: 'DevTeam',
tags: ['network', 'async', 'rest']
},
{
async get(url: string) {
return `GET ${url}`;
},
async post(url: string, data: any) {
return `POST ${url}`;
}
}
);
console.log(HTTPClient.getInfo());
// "HTTPClient v2.0.0 by DevTeam [network, async, rest]"
HTTPClient.addTag('production-ready');
console.log(HTTPClient.hasTag('async')); // true
const client = new HTTPClient();
console.log(await client.get('/api/users')); // "GET /api/users"
๐ก๏ธ Type Safety with Class Expressions
๐ Type Inference and Constraints
Ensuring type safety with dynamic classes:
// ๐ฏ Type-safe class factory with constraints
interface Plugin<T = any> {
name: string;
install(instance: T): void;
}
const createPluggableClass = <TPlugins extends Plugin[]>(
name: string,
plugins: TPlugins
) => {
return class Pluggable {
static className = name;
private installedPlugins = new Set<string>();
constructor() {
// Install all plugins
plugins.forEach(plugin => {
plugin.install(this);
this.installedPlugins.add(plugin.name);
});
}
hasPlugin(name: string): boolean {
return this.installedPlugins.has(name);
}
getPlugins(): string[] {
return Array.from(this.installedPlugins);
}
};
};
// ๐ Define plugins with proper typing
const loggingPlugin: Plugin = {
name: 'logging',
install(instance: any) {
instance.log = (message: string) => {
console.log(`[${new Date().toISOString()}] ${message}`);
};
}
};
const validationPlugin: Plugin = {
name: 'validation',
install(instance: any) {
instance.validate = (data: any, schema: any) => {
// Validation logic
return true;
};
}
};
// ๐๏ธ Create class with plugins
const MyService = createPluggableClass('MyService', [
loggingPlugin,
validationPlugin
]);
const service = new MyService();
console.log(service.hasPlugin('logging')); // true
console.log(service.getPlugins()); // ['logging', 'validation']
// Access plugin methods (with type casting for now)
(service as any).log('Service initialized');
๐ฎ Hands-On Exercise
Letโs build a game entity system with dynamic class creation!
๐ Challenge: Dynamic Game Entity System
Create a system that:
- Dynamically creates entity classes based on components
- Supports runtime class composition
- Allows entity evolution during gameplay
- Maintains type safety
// Your challenge: Implement this system
interface Component {
name: string;
onAttach?(entity: any): void;
onUpdate?(entity: any, deltaTime: number): void;
onDetach?(entity: any): void;
}
interface EntityClass {
new (id: string): Entity;
addComponent(component: Component): EntityClass;
removeComponent(componentName: string): EntityClass;
getComponents(): string[];
}
interface Entity {
id: string;
update(deltaTime: number): void;
hasComponent(name: string): boolean;
}
// Example components to implement
const HealthComponent: Component = {
name: 'health',
onAttach(entity) {
entity.health = 100;
entity.maxHealth = 100;
entity.takeDamage = (amount: number) => {
entity.health = Math.max(0, entity.health - amount);
};
entity.heal = (amount: number) => {
entity.health = Math.min(entity.maxHealth, entity.health + amount);
};
}
};
const MovementComponent: Component = {
name: 'movement',
onAttach(entity) {
entity.x = 0;
entity.y = 0;
entity.speed = 5;
entity.move = (dx: number, dy: number) => {
entity.x += dx * entity.speed;
entity.y += dy * entity.speed;
};
},
onUpdate(entity, deltaTime) {
// Update position based on velocity
}
};
// Implement the createEntityClass function!
const createEntityClass = (baseComponents: Component[] = []): EntityClass => {
// Your implementation here
};
๐ก Solution
Click to see the solution
const createEntityClass = (baseComponents: Component[] = []): EntityClass => {
const components = new Map<string, Component>(
baseComponents.map(c => [c.name, c])
);
const EntityClass = class implements Entity {
static components = components;
id: string;
constructor(id: string) {
this.id = id;
// Attach all components
EntityClass.components.forEach(component => {
component.onAttach?.(this);
});
}
update(deltaTime: number): void {
EntityClass.components.forEach(component => {
component.onUpdate?.(this, deltaTime);
});
}
hasComponent(name: string): boolean {
return EntityClass.components.has(name);
}
static addComponent(component: Component): EntityClass {
const newComponents = [...baseComponents, component];
return createEntityClass(newComponents);
}
static removeComponent(componentName: string): EntityClass {
const newComponents = baseComponents.filter(c => c.name !== componentName);
return createEntityClass(newComponents);
}
static getComponents(): string[] {
return Array.from(EntityClass.components.keys());
}
} as any as EntityClass;
return EntityClass;
};
// ๐ฎ Advanced component system
const PhysicsComponent: Component = {
name: 'physics',
onAttach(entity) {
entity.vx = 0;
entity.vy = 0;
entity.gravity = 9.8;
entity.applyForce = (fx: number, fy: number) => {
entity.vx += fx;
entity.vy += fy;
};
},
onUpdate(entity, deltaTime) {
if (entity.hasComponent('movement')) {
entity.vy += entity.gravity * deltaTime;
entity.move(entity.vx * deltaTime, entity.vy * deltaTime);
}
}
};
const CombatComponent: Component = {
name: 'combat',
onAttach(entity) {
entity.attackPower = 10;
entity.defense = 5;
entity.attack = (target: any) => {
if (target.hasComponent('health')) {
const damage = Math.max(1, entity.attackPower - (target.defense || 0));
target.takeDamage(damage);
console.log(`${entity.id} attacks ${target.id} for ${damage} damage!`);
}
};
}
};
// ๐๏ธ Create entity classes
let Player = createEntityClass([HealthComponent, MovementComponent]);
console.log(Player.getComponents()); // ['health', 'movement']
// Add combat abilities
Player = Player.addComponent(CombatComponent);
console.log(Player.getComponents()); // ['health', 'movement', 'combat']
// Create different enemy types
const BasicEnemy = createEntityClass([HealthComponent]);
const FlyingEnemy = createEntityClass([HealthComponent, MovementComponent, PhysicsComponent]);
const EliteEnemy = createEntityClass([HealthComponent, MovementComponent, CombatComponent]);
// ๐ซ Game simulation
const player = new Player('Player1');
const enemy = new EliteEnemy('Enemy1');
console.log(`Player health: ${(player as any).health}`); // 100
(enemy as any).attack(player);
console.log(`Player health after attack: ${(player as any).health}`); // 95
// Update physics
const flyingEnemy = new FlyingEnemy('Flyer1');
(flyingEnemy as any).applyForce(5, -10);
flyingEnemy.update(0.016); // 60 FPS
console.log(`Flying enemy position: ${(flyingEnemy as any).x}, ${(flyingEnemy as any).y}`);
๐ฏ Summary
Youโve mastered class expressions in TypeScript! ๐ You learned how to:
- ๐จ Create classes dynamically at runtime
- ๐ญ Build powerful factory functions
- ๐ Implement conditional class creation
- ๐งฌ Design evolving class hierarchies
- ๐ Create plugin systems with dynamic composition
- โจ Maintain type safety with dynamic patterns
Class expressions unlock powerful metaprogramming capabilities, allowing you to create flexible, reusable patterns that adapt to runtime conditions!