In TypeScript, as in JavaScript, the singleton pattern does not exist as you might know it from Java or another language.
Why don't we have the same concept?
Because objects and classes are not interdependent.
First lets ask ourselves what is a singleton?
A class that can have only one instance globally across an entire application.
In TypeScript, you would do that by creating a global variable.
For example:
// at global scope
var instance = {
prodMode: true
};
or from within a module:
globalThis.instance = {
prodMode: true
};
declare global {
var instance: {
prodMode: boolean
};
}
There it is, no classes, no locks or guards, just a global variable.
The above approach has the following additional advantages (just off the top of my head):
- It is a singleton by definition
- It is very simple.
- The object can used conveniently and unceremoniously.
Now you may respond that you need or at least want a class.
No problem:
globalThis.instance = new class {
prodMode = true
}();
But please do not use classes for simple configuration objects.
Now on to your use case of configuring the instance:
If you want to have a configurable singleton, you should consider your design carefully.
But if, after much thought, it seems necessary to create a global constructor function consider the following adjustment:
namespace singleton {
class Singleton_ {constructor(options: {}){}}
export type Singleton = Singleton_;
var instance: Singleton = undefined;
export function getInstance(options: {}) {
instance = instance || new Singleton_(options);
return instance;
}
The above approach, which uses the IIFE pattern (a TypeScript namespace
) has the following advantages:
- There is no need to write a
private constructor
to prevent external instantiation.
- The class cannot be directly instantiated (although it is accessible as
instance.constructor
which is another reason to avoid a class and use a simple object as described earlier).
- There is no
class
that can be extended or otherwise misused in any way which would break the singleton pattern at runtime (although it is accessible as instance.constructor
which is another reason to avoid a class and use a simple object as described earlier).
But as you say, passing options to the getInstance
function makes the API unclear.
If the intent is rather to allow the instance to be changed, simply expose a reconfigure method (or just make the object mutable).
globalThis.instance = {
prodMode: true,
reconfigure(options) {
this.prodMode = options.prodMode;
}
};
Remarks:
Some examples of well known singletons in JavaScript include
- The Object object
- The Array object
- The Function object
- (Your favorite library) when loaded via a script tag
Globals are generally bad and mutable globals are generally worse.
Modules can help here.
Personally, since I use modules, if I wanted a different global object depending on the value of something like "is in production", I would write
// app.ts
export {}
(async function run() {
const singletonConditionalModuleSpecifer = prodMode
? "./prod-singleton"
: "./dev-singleton";
const singleton = await import(singletonConditionalModuleSpecifer);
// use singleton
}());