Newsletter

JavaScript Singleton pattern: ES6 class, module, TypeScript (and PHP)

The Singleton pattern guarantees a single instance. Three JavaScript implementations, all run (class with a private static field, constructor that returns the instance, ES module), the TypeScript version, the PHP equivalent, and the cases where you are better off without it.

Singleton pattern in JavaScript: one instance with an ES6 class
Quick answer

The Singleton guarantees that a class has only one instance and provides a global access point to it. In modern JavaScript, the simplest way is a private static field and a getInstance() method: static #instance; static getInstance() { return Database.#instance ??= new Database(); }. An ES module that exports an object is already a singleton, and is often the best answer.

The Singleton is probably the best-known and most debated design pattern. Its promise is simple: a class that can have only one instance, accessible from anywhere. A database connection, a logger or the application’s configuration are the classic examples. In JavaScript it takes a few lines, and the language even offers a free alternative: the module. Here are the implementations that work today, along with their pitfalls.

The Singleton in one sentence

A Singleton prevents the creation of several instances of a class and provides a single access point to the existing instance. The first call creates the object; the following ones return the same one. You use it when a resource genuinely has to be shared: opening ten connections to the same database because ten modules called new Database() would be wasteful, and a log split across ten files would be unusable.

It is discouraged as soon as it only serves to avoid passing a parameter: it then becomes a global variable in disguise, hard to test and to replace. We come back to this at the end of the article.

Implementation 1: a class with a private static field

This is the recommended modern form. The #instance field is private and static: it belongs to the class, not to the objects, and nobody can read or overwrite it from outside. getInstance() creates the instance on the first call thanks to the ??= operator (“assign if nullish”).

database.js
class Database {
  static #instance;

  constructor(dsn) {
    this.dsn = dsn;
    this.connectedAt = new Date();
  }

  static getInstance(dsn = 'mongodb://localhost') {
    Database.#instance ??= new Database(dsn);
    return Database.#instance;
  }

  query(sql) {
    return `${this.dsn} > ${sql}`;
  }
}

const a = Database.getInstance('mongodb://prod');
const b = Database.getInstance('mysql://autre');

console.log(a === b);   // true  : same object
console.log(b.dsn);     // 'mongodb://prod' : the second dsn was ignored

The last line illustrates a pitfall of the pattern: the parameters of the second call are silently lost. If your singleton takes parameters, either they come from a single source (the configuration), or getInstance() throws an error when it is given different ones.

Nothing yet prevents writing new Database() directly. JavaScript has no private constructor; you simulate one with a token known only to the class:

javascript
const cle = Symbol('Database');

class Database {
  static #instance;

  constructor(jeton) {
    if (jeton !== cle) {
      throw new Error('Utilisez Database.getInstance()');
    }
  }

  static getInstance() {
    return (Database.#instance ??= new Database(cle));
  }
}

new Database();   // Error: Utilisez Database.getInstance()

Implementation 2: the constructor returns the existing instance

This is the historical version from this article, and it still works: if a constructor explicitly returns an object, new returns that object instead of the new one. The instance is stored in a static property.

database-constructeur.js
class Database {
  constructor(data) {
    if (Database.instance) {
      return Database.instance;   // new returns the existing object
    }
    this._data = data;
    Database.instance = this;
  }

  getData() {
    return this._data;
  }

  setData(data) {
    this._data = data;
  }
}

const mongo = new Database('mongo');
console.log(mongo.getData()); // 'mongo'

const mysql = new Database('mysql');
console.log(mysql.getData()); // 'mongo' : it is the same object as mongo
console.log(mongo === mysql); // true

Its advantage is that it leaves the new Database() syntax untouched for the calling code. Its drawback is that it surprises: a new that creates nothing goes against the intuition of whoever reads the code, and Database.instance is public, hence modifiable. The private-field version is more explicit.

Implementation 3: an ES module is already a singleton

A JavaScript module is evaluated only once per application, however many files import it. Exporting an instance is therefore enough to get a singleton, with no class and no getInstance().

config.js
const config = Object.freeze({
  env: process.env.NODE_ENV ?? 'development',
  apiUrl: 'https://api.example.com',
});

export default config;
logger.js
class Logger {
  #lines = [];
  log(message) {
    this.#lines.push(`${new Date().toISOString()} ${message}`);
  }
  get count() {
    return this.#lines.length;
  }
}

export const logger = new Logger();   // a single instance for the whole application
app.js
import { logger } from './logger.js';
import config from './config.js';

logger.log(`Démarrage en ${config.env}`);

Every file that imports logger receives the same object. It is the solution to prefer in most projects: it is readable, free of magic, and the module system guarantees uniqueness. Two limits: the instance is created when the module loads, even if nobody uses it, and the singleton is only unique per file path (two copies of a package in node_modules make two instances).

TypeScript version

TypeScript adds what JavaScript lacks: a genuinely private constructor, which forbids new at compile time.

database.ts
class Database {
  private static instance: Database | undefined;

  private constructor(private readonly dsn: string) {}

  static getInstance(dsn = 'mongodb://localhost'): Database {
    Database.instance ??= new Database(dsn);
    return Database.instance;
  }

  query(sql: string): string {
    return `${this.dsn} > ${sql}`;
  }
}

const db = Database.getInstance();
// new Database('x');  // Error TS2673: the constructor is private

The PHP equivalent

The structure is identical: a static property, a private constructor, a getInstance() method. PHP adds two useful locks, a private __clone and a __wakeup that refuses deserialisation, so that no copy can ever appear.

Database.php
final class Database
{
    private static ?Database $instance = null;

    private function __construct(private readonly string $dsn)
    {
    }

    public static function getInstance(string $dsn = 'mysql:host=localhost'): Database
    {
        return self::$instance ??= new self($dsn);
    }

    public function query(string $sql): string
    {
        return "{$this->dsn} > {$sql}";
    }

    private function __clone()
    {
    }

    public function __wakeup(): void
    {
        throw new LogicException('Un singleton ne se désérialise pas.');
    }
}

$a = Database::getInstance('mysql:host=prod');
$b = Database::getInstance();

var_dump($a === $b);   // bool(true)
echo $a->query('SELECT 1'); // mysql:host=prod > SELECT 1

When not to use a Singleton

The pattern is criticised for good reasons, and you should know them before adopting it:

  • It hides dependencies. A function that calls Database.getInstance() in the middle of its code depends on the database without saying so in its signature. Passing the connection as a parameter (dependency injection) makes the dependency visible and replaceable.
  • It complicates tests. State survives from one test to the next. If you keep a singleton, add a static reset() method reserved for tests, or test through injection.
  • It is global state. Everything that is true of global variables is true of the singleton: coupling, action at a distance, initialisation order.

The practical rule: a module that exports an instance (implementation 3) for configuration and stateless utilities; dependency injection for anything that touches an external resource; and the getInstance() class when a library imposes that single access point. The guard clause pattern and classes in JavaScript extend this topic on the code-structure side.

Common errors

An instance that cannot be reset in tests A singleton keeps its state from one test to the next. Provide a static reset() method reserved for tests, or inject the dependency rather than calling getInstance() in business code.
Parameters ignored on the second call new Database('mysql') returns the instance created with “mongo”: the arguments of the second call are lost without warning. Document it, or throw an error if the parameters differ.
One singleton per copy of the module Two versions of the same package installed in node_modules give two modules, hence two “singletons”. An ES module is only unique for a given file path.
Code run at import time An instance created when the module loads is built even if nobody uses it. Prefer lazy instantiation inside getInstance().

JavaScript

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.