TypeScript Singleton Pattern Implementation
The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. This is incredibly useful for managing shared resources, configuration objects, or services that should be uniquely instantiated across an application, preventing multiple copies and potential inconsistencies.
Problem Description
Your task is to implement the Singleton pattern in TypeScript for various scenarios. You'll need to create a mechanism that guarantees a class can only be instantiated once and that all calls to create an instance return the same, single object.
Key Requirements:
- Single Instance Guarantee: No matter how many times you attempt to create an instance of a Singleton class, only one actual object should ever be created.
- Global Access: Provide a consistent and straightforward way to access the single instance of the class.
- TypeScript Type Safety: Implement this pattern using TypeScript's type system, ensuring type safety and leveraging its features.
Expected Behavior:
- When you first instantiate a Singleton class, a new instance is created.
- Subsequent attempts to instantiate the same class must return the previously created instance.
- You should be able to verify that two different calls to "instantiate" the Singleton result in the exact same object in memory.
Edge Cases to Consider:
- Concurrency: While not strictly required for a basic implementation, be mindful of how this pattern might behave in concurrent environments (though for typical JavaScript/TypeScript client-side execution, this is less of a concern than in multi-threaded languages).
- Initialization: How should the singleton be initialized? Should it be lazily initialized (created only when first accessed) or eagerly initialized (created when the module loads)? Your solution should demonstrate at least one of these.
Examples
Example 1: Basic Singleton
Consider a Logger class that should have only one instance to manage console output.
// Assume this is the class definition you are creating
class Logger {
private static instance: Logger | null = null;
private messages: string[] = [];
private constructor() {
// Private constructor to prevent direct instantiation
console.log("Logger instance created.");
}
public static getInstance(): Logger {
if (Logger.instance === null) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
this.messages.push(message);
console.log(`[LOG]: ${message}`);
}
public getMessages(): string[] {
return [...this.messages];
}
}
// --- How to use it ---
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("First log message");
logger2.log("Second log message");
console.log(logger1 === logger2); // Should output: true
console.log(logger1.getMessages()); // Should output: ["First log message", "Second log message"]
Output of the usage code:
Logger instance created.
[LOG]: First log message
[LOG]: Second log message
true
[ 'First log message', 'Second log message' ]
Explanation:
Logger.getInstance() is called twice. The first call creates the Logger instance because Logger.instance is null. The second call returns the already existing Logger.instance. logger1 and logger2 therefore refer to the same object, as confirmed by logger1 === logger2.
Example 2: Singleton with Parameters (Lazy Initialization)
Imagine a ConfigService that needs to be initialized with a base URL, but we want to ensure it's only initialized once.
// Assume this is the class definition you are creating
class ConfigService {
private static instance: ConfigService | null = null;
private baseUrl: string;
private initialized: boolean = false;
private constructor() {
// Private constructor
console.log("ConfigService instance created.");
}
public static getInstance(): ConfigService {
if (ConfigService.instance === null) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
public initialize(url: string): void {
if (!this.initialized) {
this.baseUrl = url;
this.initialized = true;
console.log(`ConfigService initialized with URL: ${this.baseUrl}`);
} else {
console.warn("ConfigService already initialized. Ignoring subsequent initialization.");
}
}
public getBaseUrl(): string {
if (!this.initialized) {
throw new Error("ConfigService has not been initialized yet.");
}
return this.baseUrl;
}
}
// --- How to use it ---
const config1 = ConfigService.getInstance();
const config2 = ConfigService.getInstance();
config1.initialize("https://api.example.com");
config2.initialize("https://another.api.com"); // This initialization should be ignored
console.log(config1 === config2); // Should output: true
console.log(config1.getBaseUrl()); // Should output: "https://api.example.com"
Output of the usage code:
ConfigService instance created.
ConfigService initialized with URL: https://api.example.com
ConfigService already initialized. Ignoring subsequent initialization.
true
https://api.example.com
Explanation:
ConfigService.getInstance() is called twice, returning the same instance. The first call to initialize sets the baseUrl. The second call to initialize on the same instance is prevented by the initialized flag.
Constraints
- Your solution must be written in TypeScript.
- The Singleton classes should not be directly instantiable using
new ClassName(). - The provided examples must work correctly with your implementation.
- Aim for a clean and idiomatic TypeScript implementation.
Notes
- Consider using a
staticproperty to hold the single instance of the class. - Make the constructor
privateto prevent external instantiation. - Provide a
staticmethod (commonly namedgetInstanceor similar) to retrieve the instance. - You can choose between lazy initialization (instance created on first
getInstancecall) or eager initialization (instance created when the module loads). The examples demonstrate lazy initialization. - Think about how to handle potential initialization parameters for your singleton.