Singleton Design Pattern in TypeScript (Part 1): Motivation, Structure & Consequences

Hooman Momtaheni
4 min readMay 14, 2024

--

In this series of articles, I intend to clarify the specific principles mentioned in the book “Design Patterns: Elements of Reusable Object-Oriented Software”. I tried to explain the difficult areas with examples from the TypeScript language.

Intent

Ensure a class only has one instance, and provide a global point of access to it.

Motivation

It’s important for some classes to have exactly one instance. How do we ensure that a class has only one instance and that the instance is easily accessible? A global variable makes an object accessible, but it doesn’t keep you from instantiating multiple objects.

A better solution is to make the class itself responsible for keeping track of its sole instance. The class can ensure that no other instance can be created (by intercepting requests to create new objects), and it can provide a way to access the instance. This is the Singleton pattern.

class Logger {
private static instance: Logger;

protected constructor() {
// Prevent instantiation from outside
}

static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}

log(message: string): void {
console.log(`[LOG] ${message}`);
}
}

// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

console.log(logger1 === logger2); // Output: true

logger1.log("This is a log message.");
logger2.log("Another log message.");

Applicability

Use the Singleton pattern:

  • When there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point.
  • When the sole instance should be extensible by subclassing.

Structure

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

Participants

  • Singleton
    -
    defines an Instance operation that lets clients access its unique instance. Instance is a method.
    - may be responsible for creating its own unique instance in the cases that class not created instance yet.

Collaborations

Clients access a Singleton instance solely through Singleton’s Instance operation.

Consequences

  1. Controlled access to sole instance. Because the Singleton class encapsulates its sole instance, it can have strict control over how and when clients access it.
    For example, bellow code demonstrates how the Singleton pattern can be adapted to limit access to a specific number of instances, providing strict control over when clients can access the class:
class LimitedSingleton {
private static instanceCount: number = 0;
private static instance: LimitedSingleton | null = null;

protected constructor() {
// Private constructor to prevent instantiation
}

static getInstance(): LimitedSingleton | null {
if (LimitedSingleton.instanceCount < 2) {
if (!LimitedSingleton.instance) {
LimitedSingleton.instance = new LimitedSingleton();
}
LimitedSingleton.instanceCount++;
return LimitedSingleton.instance;
} else {
console.log("Access denied. Maximum instances reached.");
return null;
}
}
}

// Usage
const instance1 = LimitedSingleton.getInstance();
const instance2 = LimitedSingleton.getInstance();
const instance3 = LimitedSingleton.getInstance(); // Access denied. Maximum instances reached.

console.log(instance1 === instance2); // Output: true
console.log(instance2 === instance3); // Output: false

2. Reduced name space. The Singleton pattern is an improvement over global variable. It avoids polluting the name space with global variables that store sole instances because we don’t need to define new variable to store instance.

3. Permits refinement of operations and representation. The Singleton class as we defined may be subclassed, and it’s easy to configure an application with an instance of this extended class. You can configure the application with an instance of the class you need at run-time.

Example:

class Singleton {
private static instance: Singleton;

protected constructor() {
// Prevent instantiation from outside
}

static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}

// Example method
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}

class SubSingleton extends Singleton {
// Additional properties or methods can be added here
}

// Usage
const instance1 = SubSingleton.getInstance();
const instance2 = SubSingleton.getInstance();

console.log(instance1 === instance2); // Output: true

instance1.log("Log message from SubSingleton instance.");

4. Permits a variable number of instances. The pattern makes it easy to change your mind and allow more than one instance of the Singleton class. Moreover, you can use the same approach to control the number of instances that the application uses. Only the operation that grants access to the Singleton instance needs to change.

class Singleton {
private static instances: Singleton[] = [];
private static maxInstances: number = 1;
private static currentInstanceIndex: number = 0;

protected constructor() {
// Prevent instantiation from outside
}

static getInstance(): Singleton | null {
if (Singleton.currentInstanceIndex < Singleton.maxInstances) {
const instance = new Singleton();
Singleton.instances.push(instance);
Singleton.currentInstanceIndex++;
return instance;
} else {
console.log("Access denied. Maximum instances reached.");
return null;
}
}

// Example method
log(message: string): void {
console.log(`[LOG] ${message}`);
}

static setMaxInstances(max: number): void {
Singleton.maxInstances = max;
}
}

// Usage
Singleton.setMaxInstances(2);

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
const instance3 = Singleton.getInstance(); // Access denied. Maximum instances reached.

console.log(instance1 === instance2); // Output: false

instance1?.log("Log message from instance1.");
instance2?.log("Log message from instance2.");

5. More flexible than class static methods. Another way to package a singleton’s functionality is to use static class operations but this language techniques make it hard to change a design to allow more than one instance of a class. Moreover, static methods can’t override them polymorphically in some languages (in TypeScript, static methods cannot be overridden by subclassing. When a subclass defines a static method with the same signature as a static method in its superclass, it does not override the superclass’s static method; instead, it shadows it. This means that the subclass’s static method is only accessible through the subclass itself, and it does not replace or modify the behavior of the superclass’s static method).

class SuperClass {
static staticMethod() {
console.log("Static method in SuperClass");
}
}

class SubClass extends SuperClass {
static staticMethod() {
console.log("Static method in SubClass");
}
}

// Usage
SuperClass.staticMethod(); // Output: "Static method in SuperClass"
SubClass.staticMethod(); // Output: "Static method in SubClass"

In the next part we will discuss Implementation and Related Patterns of Singleton design pattern.

--

--

Hooman Momtaheni

Full Stack Web Application Developer | Admire foundations and structures | Helping companies have reliable web apps and motivated software development team