Prerequisites
- Understanding of constructors ๐
- Knowledge of access modifiers ๐
- Familiarity with static methods โก
What you'll learn
- Understand protected constructor pattern ๐ฏ
- Implement singleton and factory patterns ๐๏ธ
- Control object creation effectively ๐ก๏ธ
- Apply advanced instantiation patterns โจ
๐ฏ Introduction
Welcome to the fascinating world of controlled instantiation! ๐ In this guide, weโll explore how the protected constructor pattern gives you superpowers over object creation, allowing you to decide exactly how and when instances of your classes come to life.
Youโll discover how protected constructors are like exclusive VIP passes ๐ซ - they restrict who can create objects while still allowing inheritance. Whether youโre implementing singletons ๐, building factory patterns ๐ญ, or creating fluent builders ๐ง, understanding protected constructors is essential for advanced TypeScript design patterns.
By the end of this tutorial, youโll be confidently controlling object creation like a master architect! Letโs dive in! ๐โโ๏ธ
๐ Understanding Protected Constructors
๐ค What is the Protected Constructor Pattern?
The protected constructor pattern is like having a private club ๐ฐ where only members (subclasses) and the class itself can create new instances. Think of it as a bouncer at the door who only lets in specific people.
In TypeScript terms, a protected constructor:
- โจ Cannot be called with
new
from outside the class - ๐ Can be called by subclasses using
super()
- ๐ก๏ธ Can be called by static methods within the same class
- ๐ง Enables controlled instantiation patterns
๐ก Why Use Protected Constructors?
Hereโs why developers love protected constructors:
- Singleton Pattern ๐: Ensure only one instance exists
- Factory Pattern ๐ญ: Control how objects are created
- Builder Pattern ๐ง: Create complex objects step by step
- Validation ๐ก๏ธ: Ensure objects are always valid
Real-world example: Imagine a database connection pool ๐. You donโt want anyone creating connections directly - they should go through a controlled factory that manages the pool size and connection lifecycle.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a basic protected constructor:
// ๐ฐ Class with protected constructor
class Castle {
private name: string;
private defenders: number;
// ๐ Protected constructor - can't be instantiated directly
protected constructor(name: string, defenders: number) {
this.name = name;
this.defenders = defenders;
console.log(`๐ฐ Castle ${name} constructed with ${defenders} defenders`);
}
// ๐ญ Static factory method - the only way to create a castle
static createCastle(name: string, defenders: number): Castle {
// ๐ก๏ธ Validation before creation
if (defenders < 10) {
throw new Error('A castle needs at least 10 defenders! โ๏ธ');
}
if (name.length < 3) {
throw new Error('Castle name must be at least 3 characters! ๐');
}
// โ
Create instance using protected constructor
return new Castle(name, defenders);
}
// ๐ฐ Castle methods
getInfo(): string {
return `๐ฐ ${this.name} - Defenders: ${this.defenders}`;
}
addDefenders(count: number): void {
this.defenders += count;
console.log(`โ๏ธ Added ${count} defenders. Total: ${this.defenders}`);
}
}
// โ This would error: Constructor of class 'Castle' is protected
// const castle1 = new Castle('Winterfell', 100);
// โ
Correct way - use factory method
const castle2 = Castle.createCastle('Winterfell', 100);
console.log(castle2.getInfo());
// โ This throws error - validation fails
try {
const weakCastle = Castle.createCastle('Hut', 5);
} catch (error) {
console.log(`โ Error: ${error.message}`);
}
๐ก Explanation: The protected constructor prevents direct instantiation, forcing users to go through our validated factory method!
๐ฏ Singleton Pattern Implementation
Hereโs how to implement the classic singleton pattern:
// ๐ Singleton class - only one instance allowed
class GameManager {
private static instance: GameManager | null = null;
private score: number = 0;
private level: number = 1;
private playerName: string;
// ๐ Protected constructor prevents direct instantiation
protected constructor(playerName: string) {
this.playerName = playerName;
console.log(`๐ฎ Game Manager initialized for ${playerName}`);
}
// ๐ Get the single instance
static getInstance(playerName?: string): GameManager {
if (!GameManager.instance) {
if (!playerName) {
throw new Error('Player name required for first initialization! ๐');
}
GameManager.instance = new GameManager(playerName);
}
return GameManager.instance;
}
// ๐ฎ Game methods
addScore(points: number): void {
this.score += points;
console.log(`๐ฏ Score: ${this.score} (+${points})`);
// Level up every 100 points
const newLevel = Math.floor(this.score / 100) + 1;
if (newLevel > this.level) {
this.levelUp();
}
}
private levelUp(): void {
this.level++;
console.log(`๐ LEVEL UP! Now at level ${this.level}`);
}
getStats(): string {
return `๐ค ${this.playerName} | ๐ฏ Score: ${this.score} | ๐ Level: ${this.level}`;
}
// ๐ Reset the singleton (useful for testing)
static resetInstance(): void {
GameManager.instance = null;
console.log('๐ Game Manager reset');
}
}
// ๐ฎ Using the singleton
const game1 = GameManager.getInstance('Player1');
game1.addScore(50);
game1.addScore(60); // This triggers level up
// ๐ฏ Same instance returned
const game2 = GameManager.getInstance(); // No name needed - already initialized
console.log(game1 === game2); // true - same instance!
console.log(game2.getStats());
// โ Can't create new instance
// const game3 = new GameManager('Player2'); // Error: Constructor is protected
๐ก Practical Examples
๐ญ Example 1: Advanced Factory Pattern
Letโs create a vehicle factory with protected constructors:
// ๐ Abstract base vehicle class
abstract class Vehicle {
protected brand: string;
protected model: string;
protected year: number;
protected mileage: number = 0;
// ๐ Protected constructor - only subclasses can call
protected constructor(brand: string, model: string, year: number) {
this.brand = brand;
this.model = model;
this.year = year;
}
abstract getType(): string;
abstract getMaxSpeed(): number;
getInfo(): string {
return `${this.year} ${this.brand} ${this.model} (${this.getType()})`;
}
drive(distance: number): void {
this.mileage += distance;
console.log(`๐ Drove ${distance} miles. Total mileage: ${this.mileage}`);
}
}
// ๐ Car implementation
class Car extends Vehicle {
private doors: number;
private fuelType: 'gas' | 'electric' | 'hybrid';
// ๐ Protected constructor
protected constructor(brand: string, model: string, year: number, doors: number, fuelType: 'gas' | 'electric' | 'hybrid') {
super(brand, model, year);
this.doors = doors;
this.fuelType = fuelType;
}
// ๐ญ Static factory methods for different car types
static createSedan(brand: string, model: string, year: number): Car {
console.log('๐ Creating sedan...');
return new Car(brand, model, year, 4, 'gas');
}
static createElectricCar(brand: string, model: string, year: number): Car {
console.log('โก Creating electric car...');
return new Car(brand, model, year, 4, 'electric');
}
static createSportsCar(brand: string, model: string, year: number): Car {
console.log('๐๏ธ Creating sports car...');
return new Car(brand, model, year, 2, 'gas');
}
getType(): string {
return `${this.fuelType} car`;
}
getMaxSpeed(): number {
return this.doors === 2 ? 180 : 120; // Sports cars are faster!
}
charge(): void {
if (this.fuelType === 'electric') {
console.log('๐ Charging electric car...');
} else {
console.log('โฝ This car uses gas!');
}
}
}
// ๐๏ธ Motorcycle implementation
class Motorcycle extends Vehicle {
private engineSize: number;
// ๐ Protected constructor
protected constructor(brand: string, model: string, year: number, engineSize: number) {
super(brand, model, year);
this.engineSize = engineSize;
}
// ๐ญ Factory methods for different motorcycle types
static createSportBike(brand: string, model: string, year: number): Motorcycle {
console.log('๐๏ธ Creating sport bike...');
return new Motorcycle(brand, model, year, 1000); // 1000cc
}
static createCruiser(brand: string, model: string, year: number): Motorcycle {
console.log('๐ต Creating cruiser...');
return new Motorcycle(brand, model, year, 1800); // 1800cc
}
static createScooter(brand: string, model: string, year: number): Motorcycle {
console.log('๐ด Creating scooter...');
return new Motorcycle(brand, model, year, 150); // 150cc
}
getType(): string {
return `${this.engineSize}cc motorcycle`;
}
getMaxSpeed(): number {
if (this.engineSize >= 1000) return 160;
if (this.engineSize >= 500) return 100;
return 60; // Scooters
}
wheelie(): void {
if (this.engineSize >= 600) {
console.log('๐คธ Performing a wheelie!');
} else {
console.log('โ Not enough power for a wheelie!');
}
}
}
// ๐ญ Master Vehicle Factory
class VehicleFactory {
private static vehicleCount: number = 0;
private static vehicles: Map<string, Vehicle> = new Map();
// ๐ Create any type of vehicle
static createVehicle(type: string, brand: string, model: string, year: number): Vehicle {
let vehicle: Vehicle;
switch (type.toLowerCase()) {
case 'sedan':
vehicle = Car.createSedan(brand, model, year);
break;
case 'electric':
vehicle = Car.createElectricCar(brand, model, year);
break;
case 'sports':
vehicle = Car.createSportsCar(brand, model, year);
break;
case 'sportbike':
vehicle = Motorcycle.createSportBike(brand, model, year);
break;
case 'cruiser':
vehicle = Motorcycle.createCruiser(brand, model, year);
break;
case 'scooter':
vehicle = Motorcycle.createScooter(brand, model, year);
break;
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
// Track created vehicles
const id = `VEH_${++VehicleFactory.vehicleCount}`;
VehicleFactory.vehicles.set(id, vehicle);
console.log(`โ
Created vehicle ${id}: ${vehicle.getInfo()}`);
return vehicle;
}
// ๐ Get factory statistics
static getStats(): void {
console.log(`\n๐ญ Vehicle Factory Stats:`);
console.log(` Total vehicles created: ${VehicleFactory.vehicleCount}`);
console.log(` Vehicles in inventory: ${VehicleFactory.vehicles.size}`);
const types = new Map<string, number>();
VehicleFactory.vehicles.forEach(vehicle => {
const type = vehicle.getType();
types.set(type, (types.get(type) || 0) + 1);
});
console.log(' By type:');
types.forEach((count, type) => {
console.log(` - ${type}: ${count}`);
});
}
}
// ๐ฎ Let's create some vehicles!
const tesla = VehicleFactory.createVehicle('electric', 'Tesla', 'Model S', 2023);
const ferrari = VehicleFactory.createVehicle('sports', 'Ferrari', '488 GTB', 2023);
const harley = VehicleFactory.createVehicle('cruiser', 'Harley-Davidson', 'Road King', 2023);
const vespa = VehicleFactory.createVehicle('scooter', 'Vespa', 'Primavera', 2023);
// Use the vehicles
console.log('\n๐ Vehicle Info:');
console.log(tesla.getInfo() + ' - Max speed: ' + tesla.getMaxSpeed() + ' mph');
console.log(ferrari.getInfo() + ' - Max speed: ' + ferrari.getMaxSpeed() + ' mph');
// Type-specific methods
if (tesla instanceof Car) {
tesla.charge();
}
if (harley instanceof Motorcycle) {
harley.wheelie();
}
// Show factory stats
VehicleFactory.getStats();
๐ง Example 2: Fluent Builder Pattern
Letโs create a fluent builder with protected constructor:
// ๐ House class with complex construction
class House {
private bedrooms: number;
private bathrooms: number;
private squareFeet: number;
private hasGarage: boolean;
private hasPool: boolean;
private hasBasement: boolean;
private style: 'modern' | 'traditional' | 'colonial' | 'ranch';
private address: string;
// ๐ Protected constructor - must use builder
protected constructor(builder: HouseBuilder) {
this.bedrooms = builder.bedrooms;
this.bathrooms = builder.bathrooms;
this.squareFeet = builder.squareFeet;
this.hasGarage = builder.hasGarage;
this.hasPool = builder.hasPool;
this.hasBasement = builder.hasBasement;
this.style = builder.style;
this.address = builder.address;
}
// ๐ House methods
getDescription(): string {
const features: string[] = [];
if (this.hasGarage) features.push('Garage ๐');
if (this.hasPool) features.push('Pool ๐');
if (this.hasBasement) features.push('Basement ๐๏ธ');
return `๐ ${this.style.charAt(0).toUpperCase() + this.style.slice(1)} House at ${this.address}
๐ ${this.squareFeet} sq ft | ๐๏ธ ${this.bedrooms} bed | ๐ฟ ${this.bathrooms} bath
โจ Features: ${features.length > 0 ? features.join(', ') : 'Standard'}`;
}
calculatePrice(): number {
let basePrice = this.squareFeet * 150; // $150 per sq ft
// Style multipliers
const styleMultipliers = {
modern: 1.3,
traditional: 1.0,
colonial: 1.2,
ranch: 0.9
};
basePrice *= styleMultipliers[this.style];
// Add feature costs
if (this.hasGarage) basePrice += 25000;
if (this.hasPool) basePrice += 50000;
if (this.hasBasement) basePrice += 30000;
return Math.round(basePrice);
}
// ๐๏ธ Static method to create builder
static create(): HouseBuilder {
return new HouseBuilder();
}
}
// ๐ง House Builder class
class HouseBuilder {
// Internal properties (package visible to House)
bedrooms: number = 1;
bathrooms: number = 1;
squareFeet: number = 1000;
hasGarage: boolean = false;
hasPool: boolean = false;
hasBasement: boolean = false;
style: 'modern' | 'traditional' | 'colonial' | 'ranch' = 'traditional';
address: string = '';
// ๐ Protected to prevent direct instantiation
protected constructor() {}
// ๐๏ธ Fluent builder methods
withBedrooms(count: number): HouseBuilder {
if (count < 1 || count > 10) {
throw new Error('Bedrooms must be between 1 and 10! ๐๏ธ');
}
this.bedrooms = count;
return this;
}
withBathrooms(count: number): HouseBuilder {
if (count < 1 || count > 8) {
throw new Error('Bathrooms must be between 1 and 8! ๐ฟ');
}
this.bathrooms = count;
return this;
}
withSquareFeet(size: number): HouseBuilder {
if (size < 500 || size > 20000) {
throw new Error('Size must be between 500 and 20,000 sq ft! ๐');
}
this.squareFeet = size;
return this;
}
withGarage(): HouseBuilder {
this.hasGarage = true;
return this;
}
withPool(): HouseBuilder {
this.hasPool = true;
return this;
}
withBasement(): HouseBuilder {
this.hasBasement = true;
return this;
}
withStyle(style: 'modern' | 'traditional' | 'colonial' | 'ranch'): HouseBuilder {
this.style = style;
return this;
}
atAddress(address: string): HouseBuilder {
if (!address || address.length < 5) {
throw new Error('Valid address required! ๐');
}
this.address = address;
return this;
}
// ๐ Build the house - validation and creation
build(): House {
// Validation
if (!this.address) {
throw new Error('Address is required to build a house! ๐');
}
if (this.bathrooms > this.bedrooms + 1) {
throw new Error('Too many bathrooms for the number of bedrooms! ๐ฟ');
}
if (this.hasPool && this.squareFeet < 2000) {
throw new Error('House too small for a pool! Need at least 2000 sq ft ๐');
}
// Create house using protected constructor
console.log('๐๏ธ Building house...');
const house = new (House as any)(this); // TypeScript hack to access protected constructor
console.log('โ
House built successfully!');
return house;
}
// ๐ก Preset configurations
static starterHome(): HouseBuilder {
return new HouseBuilder()
.withBedrooms(2)
.withBathrooms(1)
.withSquareFeet(1200)
.withStyle('ranch');
}
static familyHome(): HouseBuilder {
return new HouseBuilder()
.withBedrooms(4)
.withBathrooms(3)
.withSquareFeet(2500)
.withGarage()
.withBasement()
.withStyle('traditional');
}
static luxuryHome(): HouseBuilder {
return new HouseBuilder()
.withBedrooms(5)
.withBathrooms(4)
.withSquareFeet(5000)
.withGarage()
.withPool()
.withBasement()
.withStyle('modern');
}
}
// ๐ฎ Let's build some houses!
console.log('๐๏ธ HOUSE BUILDING DEMO ๐๏ธ\n');
// Custom build
const customHouse = House.create()
.withBedrooms(3)
.withBathrooms(2)
.withSquareFeet(1800)
.withGarage()
.withStyle('colonial')
.atAddress('123 Main Street')
.build();
console.log(customHouse.getDescription());
console.log(`๐ฐ Price: $${customHouse.calculatePrice().toLocaleString()}\n`);
// Using presets
const starterHouse = HouseBuilder.starterHome()
.atAddress('456 Oak Avenue')
.build();
console.log(starterHouse.getDescription());
console.log(`๐ฐ Price: $${starterHouse.calculatePrice().toLocaleString()}\n`);
const luxuryHouse = HouseBuilder.luxuryHome()
.atAddress('789 Luxury Lane')
.build();
console.log(luxuryHouse.getDescription());
console.log(`๐ฐ Price: $${luxuryHouse.calculatePrice().toLocaleString()}\n`);
// โ Try invalid configurations
try {
const invalidHouse = House.create()
.withBedrooms(2)
.withBathrooms(5) // Too many bathrooms!
.atAddress('999 Error Street')
.build();
} catch (error) {
console.log(`โ Build failed: ${error.message}`);
}
๐ Example 3: Connection Pool Pattern
Letโs create a database connection pool with controlled instantiation:
// ๐ Database connection class
class DatabaseConnection {
private id: string;
private inUse: boolean = false;
private createdAt: Date;
private lastUsed: Date;
// ๐ Protected constructor - only pool can create
protected constructor(id: string) {
this.id = id;
this.createdAt = new Date();
this.lastUsed = new Date();
console.log(`๐ Connection ${id} created`);
}
// Friend class pattern - only ConnectionPool can create
static createForPool(id: string): DatabaseConnection {
return new DatabaseConnection(id);
}
// ๐ Connection methods
query(sql: string): void {
if (!this.inUse) {
throw new Error('Connection not acquired! ๐');
}
console.log(`๐ [${this.id}] Executing: ${sql}`);
this.lastUsed = new Date();
}
acquire(): void {
if (this.inUse) {
throw new Error('Connection already in use! โ ๏ธ');
}
this.inUse = true;
console.log(`โ
Connection ${this.id} acquired`);
}
release(): void {
this.inUse = false;
console.log(`๐ Connection ${this.id} released`);
}
isAvailable(): boolean {
return !this.inUse;
}
getInfo(): string {
const idleTime = Date.now() - this.lastUsed.getTime();
return `Connection ${this.id}: ${this.inUse ? 'IN USE' : 'AVAILABLE'} (idle: ${Math.round(idleTime / 1000)}s)`;
}
}
// ๐ Connection Pool with singleton pattern
class ConnectionPool {
private static instance: ConnectionPool | null = null;
private connections: DatabaseConnection[] = [];
private maxConnections: number;
private activeConnections: number = 0;
// ๐ Protected constructor for singleton
protected constructor(maxConnections: number = 10) {
this.maxConnections = maxConnections;
console.log(`๐ Connection pool initialized (max: ${maxConnections})`);
// Pre-create some connections
this.createInitialConnections();
}
// ๐ Get pool instance
static getInstance(maxConnections?: number): ConnectionPool {
if (!ConnectionPool.instance) {
ConnectionPool.instance = new ConnectionPool(maxConnections);
}
return ConnectionPool.instance;
}
private createInitialConnections(): void {
const initialCount = Math.min(3, this.maxConnections);
for (let i = 0; i < initialCount; i++) {
const conn = DatabaseConnection.createForPool(`CONN_${i + 1}`);
this.connections.push(conn);
}
console.log(`๐ Created ${initialCount} initial connections`);
}
// ๐ Get a connection from the pool
async getConnection(): Promise<DatabaseConnection> {
console.log('๐ Requesting connection from pool...');
// Find available connection
let connection = this.connections.find(conn => conn.isAvailable());
if (!connection) {
// Create new connection if under limit
if (this.connections.length < this.maxConnections) {
const newId = `CONN_${this.connections.length + 1}`;
connection = DatabaseConnection.createForPool(newId);
this.connections.push(connection);
console.log(`๐ Created new connection: ${newId}`);
} else {
// Wait for available connection
console.log('โณ Pool exhausted, waiting for available connection...');
connection = await this.waitForConnection();
}
}
connection.acquire();
this.activeConnections++;
return connection;
}
// โณ Wait for connection to become available
private async waitForConnection(): Promise<DatabaseConnection> {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const available = this.connections.find(conn => conn.isAvailable());
if (available) {
clearInterval(checkInterval);
resolve(available);
}
}, 100);
});
}
// ๐ Return connection to pool
releaseConnection(connection: DatabaseConnection): void {
connection.release();
this.activeConnections--;
console.log(`๐ Connection returned to pool. Active: ${this.activeConnections}/${this.connections.length}`);
}
// ๐ Pool statistics
getStats(): void {
console.log('\n๐ Connection Pool Stats:');
console.log(` Total connections: ${this.connections.length}/${this.maxConnections}`);
console.log(` Active connections: ${this.activeConnections}`);
console.log(` Available connections: ${this.connections.length - this.activeConnections}`);
console.log('\n Connection details:');
this.connections.forEach(conn => {
console.log(` - ${conn.getInfo()}`);
});
}
}
// ๐ Scoped connection helper
class ScopedConnection {
private connection: DatabaseConnection;
private pool: ConnectionPool;
private released: boolean = false;
// ๐ Protected constructor
protected constructor(connection: DatabaseConnection, pool: ConnectionPool) {
this.connection = connection;
this.pool = pool;
}
// ๐ญ Factory method
static async create(): Promise<ScopedConnection> {
const pool = ConnectionPool.getInstance();
const connection = await pool.getConnection();
return new ScopedConnection(connection, pool);
}
// ๐ Execute query
async execute(sql: string): Promise<void> {
if (this.released) {
throw new Error('Connection already released! ๐');
}
this.connection.query(sql);
}
// ๐ Auto-release
async release(): Promise<void> {
if (!this.released) {
this.pool.releaseConnection(this.connection);
this.released = true;
}
}
// ๐ฏ Execute with auto-release
static async withConnection<T>(
operation: (conn: ScopedConnection) => Promise<T>
): Promise<T> {
const scopedConn = await ScopedConnection.create();
try {
return await operation(scopedConn);
} finally {
await scopedConn.release();
}
}
}
// ๐ฎ Demo the connection pool
async function demoConnectionPool() {
console.log('๐ CONNECTION POOL DEMO ๐\n');
const pool = ConnectionPool.getInstance(5); // Max 5 connections
// Simulate multiple database operations
const operations = [
async () => {
const conn = await pool.getConnection();
conn.query('SELECT * FROM users');
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate work
pool.releaseConnection(conn);
},
async () => {
// Using scoped connection
await ScopedConnection.withConnection(async (conn) => {
await conn.execute('INSERT INTO logs VALUES (...)');
await new Promise(resolve => setTimeout(resolve, 500));
});
},
async () => {
const conn = await pool.getConnection();
conn.query('UPDATE products SET price = price * 1.1');
await new Promise(resolve => setTimeout(resolve, 800));
pool.releaseConnection(conn);
}
];
// Run operations concurrently
console.log('๐ Running concurrent database operations...\n');
await Promise.all(operations.map(op => op()));
// Show pool stats
pool.getStats();
// Demonstrate pool exhaustion
console.log('\n๐ฅ Testing pool exhaustion...');
const connections: DatabaseConnection[] = [];
try {
// Get all available connections
for (let i = 0; i < 6; i++) { // Try to get more than max
const conn = await pool.getConnection();
connections.push(conn);
}
} catch (error) {
console.log(`โ Error: ${error.message}`);
}
// Release connections
connections.forEach(conn => pool.releaseConnection(conn));
pool.getStats();
}
// Run the demo
demoConnectionPool();
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Registry Pattern
Using protected constructors with a registry:
// ๐ Plugin registry with controlled instantiation
abstract class Plugin {
protected name: string;
protected version: string;
private static registry: Map<string, Plugin> = new Map();
// ๐ Protected constructor
protected constructor(name: string, version: string) {
this.name = name;
this.version = version;
}
// ๐ Register plugin
protected register(): void {
const key = `${this.name}@${this.version}`;
if (Plugin.registry.has(key)) {
throw new Error(`Plugin ${key} already registered! ๐ซ`);
}
Plugin.registry.set(key, this);
console.log(`โ
Registered plugin: ${key}`);
}
// ๐ Get plugin from registry
static getPlugin(name: string, version: string = 'latest'): Plugin | undefined {
if (version === 'latest') {
// Find latest version
const versions = Array.from(Plugin.registry.keys())
.filter(key => key.startsWith(`${name}@`))
.map(key => key.split('@')[1])
.sort();
if (versions.length > 0) {
version = versions[versions.length - 1];
}
}
return Plugin.registry.get(`${name}@${version}`);
}
// ๐ List all plugins
static listPlugins(): void {
console.log('๐ Registered Plugins:');
Plugin.registry.forEach((plugin, key) => {
console.log(` - ${key}: ${plugin.getDescription()}`);
});
}
abstract execute(): void;
abstract getDescription(): string;
}
// ๐ Search plugin implementation
class SearchPlugin extends Plugin {
private static instances: Map<string, SearchPlugin> = new Map();
// ๐ Protected constructor
protected constructor(version: string) {
super('search', version);
}
// ๐ญ Factory with version control
static create(version: string): SearchPlugin {
const key = `search@${version}`;
if (!SearchPlugin.instances.has(key)) {
const plugin = new SearchPlugin(version);
plugin.register();
SearchPlugin.instances.set(key, plugin);
}
return SearchPlugin.instances.get(key)!;
}
execute(): void {
console.log(`๐ Executing search plugin v${this.version}`);
}
getDescription(): string {
return `Full-text search capabilities`;
}
}
// ๐ Analytics plugin
class AnalyticsPlugin extends Plugin {
private trackingId: string;
// ๐ Protected constructor
protected constructor(version: string, trackingId: string) {
super('analytics', version);
this.trackingId = trackingId;
}
// ๐ญ Singleton per tracking ID
private static instances: Map<string, AnalyticsPlugin> = new Map();
static create(version: string, trackingId: string): AnalyticsPlugin {
const key = `${trackingId}@${version}`;
if (!AnalyticsPlugin.instances.has(key)) {
const plugin = new AnalyticsPlugin(version, trackingId);
plugin.register();
AnalyticsPlugin.instances.set(key, plugin);
}
return AnalyticsPlugin.instances.get(key)!;
}
execute(): void {
console.log(`๐ Tracking with ${this.trackingId} (v${this.version})`);
}
getDescription(): string {
return `Analytics tracking for ${this.trackingId}`;
}
}
๐๏ธ Advanced Topic 2: Multi-Stage Builder
Complex builders with protected constructors:
// ๐ฌ Movie builder with type-safe stages
class Movie {
title: string;
director: string;
year: number;
genre: string;
runtime: number;
cast: string[];
budget?: number;
rating?: string;
// ๐ Protected constructor
protected constructor(builder: CompletedMovieBuilder) {
this.title = builder.title;
this.director = builder.director;
this.year = builder.year;
this.genre = builder.genre;
this.runtime = builder.runtime;
this.cast = builder.cast;
this.budget = builder.budget;
this.rating = builder.rating;
}
getInfo(): string {
return `๐ฌ "${this.title}" (${this.year})
๐ญ Director: ${this.director}
๐ฏ Genre: ${this.genre}
โฑ๏ธ Runtime: ${this.runtime} minutes
๐ฅ Cast: ${this.cast.join(', ')}
${this.budget ? `๐ฐ Budget: $${this.budget.toLocaleString()}` : ''}
${this.rating ? `โญ Rating: ${this.rating}` : ''}`;
}
}
// Stage interfaces
interface NeedsTitleStage {
withTitle(title: string): NeedsDirectorStage;
}
interface NeedsDirectorStage {
withDirector(director: string): NeedsYearStage;
}
interface NeedsYearStage {
withYear(year: number): NeedsGenreStage;
}
interface NeedsGenreStage {
withGenre(genre: string): NeedsRuntimeStage;
}
interface NeedsRuntimeStage {
withRuntime(minutes: number): NeedsCastStage;
}
interface NeedsCastStage {
withCast(...actors: string[]): OptionalStage;
}
interface OptionalStage {
withBudget(amount: number): OptionalStage;
withRating(rating: string): OptionalStage;
build(): Movie;
}
// Complete builder type
interface CompletedMovieBuilder {
title: string;
director: string;
year: number;
genre: string;
runtime: number;
cast: string[];
budget?: number;
rating?: string;
}
// ๐๏ธ Multi-stage builder implementation
class MovieBuilder implements
NeedsTitleStage,
NeedsDirectorStage,
NeedsYearStage,
NeedsGenreStage,
NeedsRuntimeStage,
NeedsCastStage,
OptionalStage,
CompletedMovieBuilder {
title!: string;
director!: string;
year!: number;
genre!: string;
runtime!: number;
cast!: string[];
budget?: number;
rating?: string;
// ๐ Protected constructor
protected constructor() {}
// ๐ญ Entry point
static create(): NeedsTitleStage {
return new MovieBuilder();
}
withTitle(title: string): NeedsDirectorStage {
this.title = title;
return this;
}
withDirector(director: string): NeedsYearStage {
this.director = director;
return this;
}
withYear(year: number): NeedsGenreStage {
if (year < 1900 || year > new Date().getFullYear() + 5) {
throw new Error('Invalid year! ๐
');
}
this.year = year;
return this;
}
withGenre(genre: string): NeedsRuntimeStage {
this.genre = genre;
return this;
}
withRuntime(minutes: number): NeedsCastStage {
if (minutes < 1 || minutes > 600) {
throw new Error('Runtime must be between 1 and 600 minutes! โฑ๏ธ');
}
this.runtime = minutes;
return this;
}
withCast(...actors: string[]): OptionalStage {
if (actors.length === 0) {
throw new Error('At least one cast member required! ๐ฅ');
}
this.cast = actors;
return this;
}
withBudget(amount: number): OptionalStage {
this.budget = amount;
return this;
}
withRating(rating: string): OptionalStage {
this.rating = rating;
return this;
}
build(): Movie {
return new (Movie as any)(this);
}
}
// ๐ฎ Demo the multi-stage builder
const movie1 = MovieBuilder.create()
.withTitle('The TypeScript Chronicles')
.withDirector('Code Cameron')
.withYear(2024)
.withGenre('Tech Thriller')
.withRuntime(120)
.withCast('Dev Patel', 'Emma Console', 'Chris Compile')
.withBudget(50000000)
.withRating('PG-13')
.build();
console.log(movie1.getInfo());
// Type safety prevents skipping stages
// const movie2 = MovieBuilder.create()
// .withTitle('Bad Movie')
// .build(); // โ Error: Property 'build' does not exist
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting to Make Constructor Protected
// โ Wrong - public constructor defeats the purpose!
class ConfigManager {
private static instance: ConfigManager;
constructor() { // Should be protected!
// Anyone can create instances
}
static getInstance(): ConfigManager {
// Singleton pattern broken!
return this.instance || (this.instance = new ConfigManager());
}
}
// โ
Correct - protected constructor
class SecureConfigManager {
private static instance: SecureConfigManager;
protected constructor() { // โ
Protected!
console.log('Config manager initialized');
}
static getInstance(): SecureConfigManager {
return this.instance || (this.instance = new SecureConfigManager());
}
}
๐คฏ Pitfall 2: Accessing Protected Constructor from Wrong Context
class Base {
protected constructor() {}
}
class Derived extends Base {
constructor() {
super(); // โ
OK - can call protected constructor
}
// โ Wrong - can't create parent instance
createParent(): Base {
return new Base(); // Error: Constructor is protected
}
// โ
Correct - use factory method
static createBase(): Base {
// Would need a factory method in Base class
throw new Error('Use Base factory method');
}
}
๐ Pitfall 3: Breaking Singleton with Inheritance
// โ Problematic - inheritance breaks singleton
class SingletonBase {
protected static instance: SingletonBase;
protected constructor() {}
static getInstance(): SingletonBase {
return this.instance || (this.instance = new SingletonBase());
}
}
class SingletonDerived extends SingletonBase {
// Now we have TWO singletons!
}
// โ
Better - composition over inheritance
class SingletonService {
private static instance: SingletonService;
private constructor() {} // Private prevents inheritance
static getInstance(): SingletonService {
return this.instance || (this.instance = new SingletonService());
}
}
๐ ๏ธ Best Practices
- ๐ฏ Clear Intent: Make it obvious why constructor is protected
- ๐ญ Provide Factory Methods: Always offer a way to create instances
- ๐ Document Creation: Explain how to properly instantiate
- ๐ก๏ธ Validate in Factories: Put validation in factory methods
- ๐ Consider Private: Use private constructor if no inheritance needed
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Configuration System
Create a configuration system with controlled instantiation:
๐ Requirements:
- โ
Protected constructor for
Config
class - ๐ Environment-specific configs (dev, staging, prod)
- ๐ Singleton pattern for each environment
- ๐ญ Factory methods with validation
- ๐ Config reloading capability
๐ Bonus Points:
- Add config versioning
- Implement config inheritance
- Create config change notifications
๐ก Solution
๐ Click to see solution
// ๐ง Configuration system with protected constructor
abstract class Config {
protected environment: string;
protected version: string;
protected data: Map<string, any> = new Map();
protected loadedAt: Date;
private listeners: ((key: string, value: any) => void)[] = [];
// ๐ Protected constructor
protected constructor(environment: string, version: string) {
this.environment = environment;
this.version = version;
this.loadedAt = new Date();
}
// ๐ฏ Abstract methods
protected abstract loadDefaults(): void;
protected abstract validate(): boolean;
// ๐ Config methods
get<T>(key: string, defaultValue?: T): T {
return this.data.has(key) ? this.data.get(key) : defaultValue;
}
set(key: string, value: any): void {
const oldValue = this.data.get(key);
this.data.set(key, value);
if (oldValue !== value) {
this.notifyListeners(key, value);
}
}
has(key: string): boolean {
return this.data.has(key);
}
// ๐ Change notifications
onChange(listener: (key: string, value: any) => void): void {
this.listeners.push(listener);
}
private notifyListeners(key: string, value: any): void {
this.listeners.forEach(listener => listener(key, value));
}
// ๐ Get config info
getInfo(): string {
return `โ๏ธ ${this.environment.toUpperCase()} Config v${this.version}
๐
Loaded: ${this.loadedAt.toLocaleString()}
๐ Entries: ${this.data.size}`;
}
// ๐ Reload configuration
reload(): void {
console.log(`๐ Reloading ${this.environment} configuration...`);
this.data.clear();
this.loadDefaults();
if (!this.validate()) {
throw new Error('Configuration validation failed! โ');
}
this.loadedAt = new Date();
console.log('โ
Configuration reloaded successfully');
}
}
// ๐ญ Development configuration
class DevelopmentConfig extends Config {
private static instance: DevelopmentConfig | null = null;
// ๐ Protected constructor
protected constructor() {
super('development', '1.0.0');
this.loadDefaults();
if (!this.validate()) {
throw new Error('Invalid development configuration! โ');
}
}
// ๐ Singleton getter
static getInstance(): DevelopmentConfig {
if (!DevelopmentConfig.instance) {
DevelopmentConfig.instance = new DevelopmentConfig();
console.log('๐ง Development configuration initialized');
}
return DevelopmentConfig.instance;
}
protected loadDefaults(): void {
// Development defaults
this.set('api.url', 'http://localhost:3000');
this.set('api.timeout', 5000);
this.set('debug', true);
this.set('logging.level', 'debug');
this.set('cache.enabled', false);
this.set('features.experimental', true);
}
protected validate(): boolean {
// Development validation is lenient
return this.has('api.url') && this.has('debug');
}
// ๐ง Dev-specific methods
enableAllFeatures(): void {
console.log('๐ Enabling all experimental features for development');
this.set('features.experimental', true);
this.set('features.beta', true);
this.set('features.alpha', true);
}
}
// ๐ข Production configuration
class ProductionConfig extends Config {
private static instance: ProductionConfig | null = null;
private securityKey: string;
// ๐ Protected constructor with security
protected constructor(securityKey: string) {
super('production', '1.0.0');
if (!this.validateSecurityKey(securityKey)) {
throw new Error('Invalid security key! ๐');
}
this.securityKey = securityKey;
this.loadDefaults();
if (!this.validate()) {
throw new Error('Invalid production configuration! โ');
}
}
// ๐ Singleton with security
static getInstance(securityKey?: string): ProductionConfig {
if (!ProductionConfig.instance) {
if (!securityKey) {
throw new Error('Security key required for first initialization! ๐');
}
ProductionConfig.instance = new ProductionConfig(securityKey);
console.log('๐ข Production configuration initialized');
}
return ProductionConfig.instance;
}
private validateSecurityKey(key: string): boolean {
// Simple validation - in reality would be more complex
return key.length >= 32 && key.includes('-PROD-');
}
protected loadDefaults(): void {
// Production defaults
this.set('api.url', 'https://api.production.com');
this.set('api.timeout', 30000);
this.set('debug', false);
this.set('logging.level', 'error');
this.set('cache.enabled', true);
this.set('cache.ttl', 3600);
this.set('security.https', true);
this.set('security.headers.hsts', true);
this.set('features.experimental', false);
}
protected validate(): boolean {
// Strict production validation
return this.has('api.url') &&
this.get('api.url').startsWith('https://') &&
this.get('debug') === false &&
this.get('security.https') === true;
}
// ๐ Production-specific methods
getSecureValue(key: string): any {
console.log(`๐ Secure access to ${key}`);
return this.get(key);
}
}
// ๐ Configuration manager
class ConfigurationManager {
private static configs: Map<string, Config> = new Map();
// ๐ญ Get configuration for environment
static getConfig(environment: 'development' | 'staging' | 'production', securityKey?: string): Config {
if (!this.configs.has(environment)) {
let config: Config;
switch (environment) {
case 'development':
config = DevelopmentConfig.getInstance();
break;
case 'production':
config = ProductionConfig.getInstance(securityKey);
break;
case 'staging':
// Staging inherits from production with overrides
config = this.createStagingConfig(securityKey);
break;
default:
throw new Error(`Unknown environment: ${environment}`);
}
this.configs.set(environment, config);
}
return this.configs.get(environment)!;
}
// ๐ญ Create staging config (hybrid)
private static createStagingConfig(securityKey?: string): Config {
// For demo, we'll create a custom staging config
class StagingConfig extends ProductionConfig {
protected constructor(securityKey: string) {
super(securityKey);
this.environment = 'staging';
// Override some production settings
this.set('api.url', 'https://api.staging.com');
this.set('debug', true); // Enable debug in staging
this.set('features.beta', true); // Enable beta features
}
static getInstance(securityKey?: string): StagingConfig {
return new StagingConfig(securityKey || 'staging-key-PROD-test');
}
}
return StagingConfig.getInstance(securityKey);
}
// ๐ Show all configurations
static showConfigs(): void {
console.log('\n๐ Loaded Configurations:');
this.configs.forEach((config, env) => {
console.log(`\n${config.getInfo()}`);
});
}
}
// ๐ฎ Demo the configuration system
function demoConfigSystem() {
console.log('๐ง CONFIGURATION SYSTEM DEMO ๐ง\n');
// Development config
const devConfig = ConfigurationManager.getConfig('development');
console.log(devConfig.getInfo());
console.log('API URL:', devConfig.get('api.url'));
console.log('Debug:', devConfig.get('debug'));
// Change listener
devConfig.onChange((key, value) => {
console.log(`๐ Config changed: ${key} = ${value}`);
});
// Modify config
devConfig.set('api.timeout', 10000);
// Production config (with security)
try {
const prodConfig = ConfigurationManager.getConfig('production', 'super-secret-key-PROD-2024');
console.log('\n' + prodConfig.getInfo());
console.log('API URL:', prodConfig.get('api.url'));
console.log('HTTPS:', prodConfig.get('security.https'));
} catch (error) {
console.log(`โ Production error: ${error.message}`);
}
// Staging config
const stagingConfig = ConfigurationManager.getConfig('staging');
console.log('\n' + stagingConfig.getInfo());
console.log('API URL:', stagingConfig.get('api.url'));
console.log('Debug:', stagingConfig.get('debug'));
console.log('Beta Features:', stagingConfig.get('features.beta'));
// Show all configs
ConfigurationManager.showConfigs();
// Reload configuration
console.log('\n๐ Reloading development config...');
devConfig.reload();
// Type-safe config access
interface ApiConfig {
url: string;
timeout: number;
}
const apiConfig: ApiConfig = {
url: devConfig.get<string>('api.url'),
timeout: devConfig.get<number>('api.timeout')
};
console.log('\n๐ก API Configuration:', apiConfig);
}
// Run the demo
demoConfigSystem();
๐ Key Takeaways
Youโve mastered the protected constructor pattern! Hereโs what you can now do:
- โ Control instantiation with protected constructors ๐
- โ Implement singleton pattern correctly ๐
- โ Create sophisticated factory methods ๐ญ
- โ Build fluent builders with type safety ๐ง
- โ Design robust object creation patterns ๐
Remember: Protected constructors give you the power to control the โwhenโ and โhowโ of object creation! ๐ฏ
๐ค Next Steps
Congratulations! ๐ Youโve mastered protected constructor patterns!
Hereโs what to do next:
- ๐ป Practice with the configuration system exercise above
- ๐๏ธ Implement your own controlled instantiation patterns
- ๐ Move on to our next tutorial: Interfaces in TypeScript: Defining Object Shapes
- ๐ Experiment with combining protected constructors and factory patterns!
Remember: Great software design often starts with controlling how objects are born. Keep building, keep controlling, and create amazing patterns! ๐
Happy coding! ๐๐โจ