The Singleton pattern is one of the most basic and powerful patterns for creating shared class instances. You may consider using the singleton pattern when you have a class that is a shared dependency for many other classes - such as an ORM(Object Relational Mapper) or database connection, which may be costly to recreate each time you need access to the database.
The basic idea behind the Singleton pattern is to store a reference to the instance of a class, and return that reference each time something asks for a new instance of that class, instead of creating a new instance each time.
We'll look at an old school example of a Javascript singleton before moving on to a typescript version, and finally a decorator that can turn almost any class into a singleton!
In traditional Javascript you might see something similar this:
1const Store = (function() {
2 const data = [];
3
4 function add(item) {
5 data.push(item);
6 }
7
8 function get(id) {
9 return data.find((d) => d.id === id);
10 }
11
12 return {
13 add, get
14 };
15}());
Note: the code above is making use of the iife pattern as well
When this code runs, Store
will be set to the return value of Store
— an object that exposes two functions, add
and get
but that does not grant direct access to the collection of data.
This is beneficial enough, but it is more verbose than we really need and it doesn't give us any safety guarantees since this is just vanilla Javascript – there's nothing to prevent us from doing nefarious things!
This code is also not reusable, which means we will have to reimplement the same code for any object that needs to be a Singleton!
For the Typescript version, we will do essentially the same thing, but with a class.
1class Store {
2 private static instance: Store;
3 private data: {id: number}[];
4 private constructor() {}
5
6 public static getInstance() {
7 if (!Store.instance) {
8 Store.instance = new Store();
9 }
10 return Store.instance;
11 }
12
13 public add(item: {id: number}) {
14 this.data.push(item);
15 }
16
17 public get(id: number) {
18 return this.data.find((d) => d.id === id);
19 }
20}
21
22const wontWork = new Store() // throws an Error: constructor of 'Store' is private
23
24const myStore = Store.getInstance();
25myStore.add({id: 1});
26myStore.add({id: 2});
27myStore.add({id: 3});
28
29const anotherStore = Store.getInstance();
30anotherStore.get(2); // returns `{id: 2}`!
This is slightly improved over the pure javascript version. We now have type safety through the Typescript compiler and I think it's a little more readable, but this code is still verbose and could be drastically simplified.
Another problem with this version is that you have to use that weird getInstance
method instead of the normal new Store()
syntax we're all used to.
So now that we've compared the vanilla Javascript method to the Typescript method, let's make the concept of a Singleton reusable! One of the best ways to accomplish this is through the use of a decorator!
The next example is going to go over how to implement a @Singleton
decorator that is reusable.
The first thing we're going to do is use a Javascript Symbol to create a unique key that represents the instance of whatever class we are converting to a singleton. We do this because otherwise, we have to worry about the possibility of overwriting a field or method name on the original class.
export const SINGLETON_KEY = Symbol();
Next we define the type for a Singleton – a class that is the same as the original class, which also uses the SINGLETON_KEY
to create a unique field that represents the instance.
1export type Singleton<T extends new (...args: any[]) => any> = T & {
2 [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
3};
Finally, we implement a function that will act as the decorator itself. There's a lot going on here, but at its core this is a simple process.
Note: You can read more about decorators and their syntax here
Our Singleton decorator function takes in a single parameter called type
. type
represents a class, before it has been initialize - a type indeed!
It then returns a new Javascript Proxy which we use to build a construct trap - essentially we hijack the original constructor of type
and replace it with our own implementation which will create the instance if it doesn't exist, or return the existing instance!
1export const Singleton = <T extends new (...args: any[]) => any>(type: T) =>
2 new Proxy(type, {
3 // this will hijack the constructor
4 construct(target: Singleton<T>, argsList, newTarget) {
5 // we should skip the proxy for children of our target class
6 if (target.prototype !== newTarget.prototype) {
7 return Reflect.construct(target, argsList, newTarget);
8 }
9 // if our target class does not have an instance, create it
10 if (!target[SINGLETON_KEY]) {
11 target[SINGLETON_KEY] = Reflect.construct(target, argsList, newTarget);
12 }
13 // return the instance we created!
14 return target[SINGLETON_KEY];
15 }
16 });
Put that all together in a file called Singleton.ts and this is what is should look like:
1export const SINGLETON_KEY = Symbol();
2
3export type Singleton<T extends new (...args: any[]) => any> = T & {
4 [SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
5};
6
7export const Singleton = <T extends new (...args: any[]) => any>(type: T) =>
8 new Proxy(type, {
9 // this will hijack the constructor
10 construct(target: Singleton<T>, argsList, newTarget) {
11 // we should skip the proxy for children of our target class
12 if (target.prototype !== newTarget.prototype) {
13 return Reflect.construct(target, argsList, newTarget);
14 }
15 // if our target class does not have an instance, create it
16 if (!target[SINGLETON_KEY]) {
17 target[SINGLETON_KEY] = Reflect.construct(target, argsList, newTarget);
18 }
19 // return the instance we created!
20 return target[SINGLETON_KEY];
21 }
22 });
Now we can use this decorator on our Store class and remove all of the Singleton specific code from it!
Note: You will need to enable the experimental decorator flags for typescript in order for this to work!
1// in tsconfig.json add the following
2 "experimentalDecorators": true,
3 "emitDecoratorMetadata": true,
1import {Singleton} from './Singleton';
2
3@Singleton
4class Store {
5 private data: {id: number}[];
6
7 public add(item: {id: number}) {
8 this.data.push(item);
9 }
10
11 public get(id: number) {
12 return this.data.find((d) => d.id === id);
13 }
14}
15
16const myStore = new Store();
17myStore.add({id: 1});
18myStore.add({id: 2});
19myStore.add({id: 3});
20
21const anotherStore = new Store();
22anotherStore.get(2); // returns `{id: 2}`!
I don't know about you, but that's much more readable to me - and as a bonus, @Singleton
is reusable across our entire codebase! We won't have to write Singleton specific logic in each class that needs to be a Singleton!
This is just one of many design patterns available to us today. If you found this article helpful, let me know in a comment or share it around!