TypeScript Singleton Pattern: The Complete Implementation Tutorial

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?

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

  1. Creating Singletons for every service
  2. Using Singletons as global variables
  3. Tight coupling through direct Singleton usage
  4. Not considering thread safety
  5. 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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap