How to make a JavaScript singleton class

First of all: why singleton? Isn’t it widely considered an antipattern?

…yes it is in most cases, and I do not want to argue with that. I can’t imagine a perfect use case when designing something as a singleton is justified. However, we are not living in an ideal world, and sometimes we have to work with vendor code that we actually have to use, and it actually uses singletons. This is where we are forced to use singletons also or at least try to wrap them up somehow to use them in a more civilized way.

For example, I started working with mainloop.js library. I really, really love it, and it actually provides good, detailed explanation on how main loops should be written. …but it comes as a singleton. It also defends its use of singletons. In the end, I think it’s a great library, and its benefits are much better than it’s drawbacks. This is the situation where I wanted to wrap it in ES6 class to add some other functionalities. Naturally, my class needs to be a singleton, so whether or not I agree with the concept, I have to adapt somehow.

Below, I’ll not present how to integrate mainloop.js specifically, rather the general concept of how singletons can be handled instead.

In general, having an actual ES6 class as a singleton will allow us to type check JavaScript code with tools like Flow or TypeScript more easily and it’s one of the main reasons for me to do it that way.

Creating a JavaScript singleton class

JavaScript has expressive ways of creating singletons. Also, it seems that private fields are coming to JavaScript. I’ll describe here how to create a singleton using JavaScript class with no private properties (which are not yet widely supported - as of 2019) instead.

Creating a base class

Let’s say we have files structure like this:

project/
├── Hello.js
└── index.js

Let’s say the singleton class is defined in Hello.js file to be used inside index.js file. It contains static getInstance() method that returns the only class instance that should be ever used.

// Hello.js

export default class Hello {
  static getInstance() {
    return instance;
  }

  getGreeting() {
    return "Hello, world!";
  }
}

const instance = new Hello();

However, this example is incomplete because you can actually create a new class instance by calling new Hello() instead of Hello.getInstance() like this:

// index.js

import Hello from "./Hello";

const hello1 = Hello.getInstance();
const hello2 = new Hello();

hello1 === hello2; // false

Final code

To prevent calling new Hello() we can add a simple check in the constructor. We will use the fact that {} creates a new JavaScript object.

Note

Thank you Tomasz Jakut for reviewing and simplifying this code in a clever way. Initial version is here: Gist

// Hello.js

// `instance` holds a unique reference that is not exported outside this file
// this is a really important variable, it needs to be privately scoped, it
// will be used to check if constructor is actually called properly
let instance;

export default class Hello {
  static getInstance() {
    return instance;
  }

  constructor() {
    // it's not possible to call this constructor outside this module, because
    // 'instance' variable is not exported
    if (instance) {
      throw new Error("Hello is a singleton. Please use `getInstance` method instead");
    }
  }

  getGreeting() {
    return "Hello, world!";
  }
}

instance = new Hello();

This time, we actually forced the use of singleton, and it’s still a class.

// index.js

import Hello from "./Hello";

// OK
const hello = Hello.getInstance();

// throws Error("Hello is a singleton (...)");
new Hello();

// this one also throws Error, because object reference is different than the
// private one
new Hello({});

Summary

The code I present above is technically correct and can be safely adapted to production environments. The same effect can be achieved in many different ways, so I do not suggest to rewrite your singletons that specific way, I do not claim it’s the best and only way to create singletons; just take a look and make your own mind on how to handle those. I hope it helped though; if you have any questions, feel free to leave those in comments.