Singleton pattern in TypeScript

In certain scenarios, there arises a necessity to execute tasks only once throughout the application's lifecycle. This could involve managing a user session, implementing a singular object to monitor state changes, or employing an error object to standardize the formatting of errors for display when they occur.

Definition

A singleton is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance, preventing the creation of multiple copies and facilitating centralized control. It is particularly useful for scenarios where a single, globally accessible object is essential, such as managing configurations, resources, or states within an application.

Implementation

Let's begin with the fundamentals – a mechanism designed to prevent the creation of multiple instances of something.

// Holds a list of initialized stuff.
const instances = new Map<string, unknown>();
// "Key" and "Value" generics hold types for key and value.
const Singleton = <Key extends string, Value>(
  key: Key,
  initializer: () => Value,
) => {
  // Assertion is required here.
  if (instances.has(key)) return instances.get(key) as Value;
  // Inits when the key does not exist.
  const instance = initializer();
  // Sets an instance and returns it.
  instances.set(key, instance);

  return instance;
};
// Creates an instance of an error object.
const error = Singleton(`error`, () => ({
  message: `something`,
  type: `error`,
}));

// This will return the first "error" instance 👆
Singleton(`error`, () => ({
  message: `something`,
  type: `error`,
}));

// Creates an instance of a modal object.
const modal = Singleton(`modal`, () => ({
  message: `something1`,
  type: `error1`,
}));

// This will return the first "modal" instance 👆
Singleton(`modal`, () => ({
  message: `something1`,
  type: `erro1r`,
}));

Now, within your codebase, you have the flexibility to initialize the object at the outset, perhaps in the index.ts file or another bootstrap initial file, and retrieve it later from any location based on the designated key.

// index.ts

const errorManager = Singleton(`error`, () => {
  let message: string | null;

  return {
    get: () => message,
    set: (newMessage: string) => {
      message = newMessage;
      return message;
    },
  };
});

// In any other file in your app code.
const msg = errorManager.get();

if (msg) {
  throw Error(msg);
}

Real-world use case

In managing configurations for development/production environments, the Singleton pattern is ideal. It prevents the simultaneous existence of two active setups during application build/runtime, averting potential issues and promoting stability.

// config.ts

const config = Singleton(`config`, () => ({
  production: process.env.mode === `production`,
  development: process.env.mode === `development`,
}));

// In other.file.ts

import { config } from '@config';

if (config.production) {
  // Do logic...
}

// In other2.file2.ts

const config = Singleton(`config`, () => ({
  production: 'XD',
  development: 'XD',
}));

// Here it returns a value from the first setup.
// So, config production will be still a bool at runtime.
if (config.production) {
}

Summary

The Singleton pattern can enhance the robustness of your codebase, particularly in large-scale projects or when dealing with singleton-like instances in your applications. It acts as a safeguard against common errors, contributing to a more secure and reliable development environment.