The Singleton pattern stands as a fundamental design pattern in TypeScript, ensuring a class maintains only one instance while offering global access. Let’s explore practical implementations and real-world applications of this essential pattern.
Table of Contents
- What is the Singleton Pattern?
- When Should You Use Singletons?
- Basic Singleton Implementation
- Thread-Safe Implementation
- Real-World Example: Configuration Manager
- Making Singletons Testable
- Advanced Patterns
- When to Skip Singletons
- Common Mistakes
- Conclusion
What is the Singleton Pattern?
A Singleton restricts a class to a single instance throughout your application’s lifecycle. Think of it as a global state manager with controlled access – like having one master key that opens a specific door.
When Should You Use Singletons?
Singletons excel in specific scenarios where global state management is crucial:
- Managing application configurations
- Controlling database connections
- Centralizing logging operations
- Handling application-wide state
- Coordinating shared resources
Basic Singleton Implementation
Here’s a straightforward TypeScript Singleton implementation:
class BasicSingleton {
private static instance: BasicSingleton;
private constructor() {
// Private constructor prevents direct instantiation
}
public static getInstance(): BasicSingleton {
if (!BasicSingleton.instance) {
BasicSingleton.instance = new BasicSingleton();
}
return BasicSingleton.instance;
}
}
// Usage
const instance1 = BasicSingleton.getInstance();
const instance2 = BasicSingleton.getInstance();
console.log(instance1 === instance2); // true
Code language: JavaScript (javascript)
Key elements explained:
- Private constructor blocks ‘new’ keyword usage
- Static instance holds the single object
- getInstance() method controls access
Thread-Safe Implementation
For applications needing thread safety:
class ThreadSafeSingleton {
private static instance: ThreadSafeSingleton;
private static isCreating: boolean = false;
private constructor() {
if (!ThreadSafeSingleton.isCreating) {
throw new Error('Please use getInstance() method.');
}
}
public static getInstance(): ThreadSafeSingleton {
if (!ThreadSafeSingleton.instance) {
ThreadSafeSingleton.isCreating = true;
ThreadSafeSingleton.instance = new ThreadSafeSingleton();
ThreadSafeSingleton.isCreating = false;
}
return ThreadSafeSingleton.instance;
}
}
Code language: PHP (php)
Real-World Example: Configuration Manager
Here’s a practical configuration manager using the Singleton pattern:
class ConfigManager {
private static instance: ConfigManager;
private config: Record<string, any> = {};
private constructor() {
// Load default configuration
this.config = {
environment: 'development',
apiTimeout: 5000
};
}
public static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
public setConfig(key: string, value: any): void {
this.config[key] = value;
}
public getConfig(key: string): any {
return this.config[key];
}
}
// Example usage
const config = ConfigManager.getInstance();
config.setConfig('apiUrl', 'https://api.example.com');
// Access from anywhere
const sameConfig = ConfigManager.getInstance();
console.log(sameConfig.getConfig('apiUrl')); // https://api.example.com
Code language: JavaScript (javascript)
Making Singletons Testable
Here’s how to create testable Singletons using dependency injection:
interface ILogger {
log(message: string): void;
getLogs(): string[];
}
class Logger implements ILogger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
this.logs.push(`${new Date().toISOString()}: ${message}`);
console.log(message);
}
public getLogs(): string[] {
return [...this.logs];
}
}
class UserService {
constructor(private logger: ILogger = Logger.getInstance()) {}
public createUser(username: string): void {
this.logger.log(`Creating user: ${username}`);
// User creation logic here
}
}
Code language: JavaScript (javascript)
Advanced Patterns
Lazy Loading
class LazySingleton {
private static instance: LazySingleton;
private constructor() {}
public static getInstance(): LazySingleton {
return this.instance || (this.instance = new LazySingleton());
}
}
Code language: JavaScript (javascript)
Immutable Configuration
class ImmutableConfig {
private static instance: ImmutableConfig;
private readonly settings: Readonly<Record<string, any>>;
private constructor() {
this.settings = Object.freeze({
version: '1.0.0',
apiUrl: 'https://api.example.com',
timeout: 5000
});
}
public static getInstance(): ImmutableConfig {
if (!ImmutableConfig.instance) {
ImmutableConfig.instance = new ImmutableConfig();
}
return ImmutableConfig.instance;
}
public getSettings(): Readonly<Record<string, any>> {
return this.settings;
}
}
Code language: JavaScript (javascript)
When to Skip Singletons
Avoid Singletons when:
- Your object doesn’t need global state
- You might need multiple instances later
- Testing independence is crucial
- You want to minimize component coupling
- Local state management works better
Common Mistakes
- Creating Singletons for every service
- Using Singletons as global variables
- Tight coupling through direct Singleton usage
- Not considering thread safety
- Skipping interface implementations
Conclusion
Singletons serve a specific purpose in TypeScript applications. Use them wisely for global state management, but consider alternatives when simpler solutions work better. Remember: the key to successful Singleton implementation lies in understanding both their benefits and limitations.
For more TypeScript patterns, check out our guide on TypeScript Decorators with Factory Functions.