The Singleton Pattern in TypeScript
When one shared instance makes sense in TypeScript, how to wire it, and why dependency injection is often the better exit.
Sometimes you only want one instance of a class. Database connection pools, shared config loaders, a logging sink. The Singleton pattern guarantees that.
Why bother?
Shared expensive resources are the classic case. One pool beats opening fifty connections. A single config reader avoids conflicting reads from disk. Centralized logging means one place to filter errors.
The pattern is seductive and easy to abuse. Know the tradeoffs before you sprinkle getInstance() everywhere.
Implementation
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
Private constructor blocks new Singleton(). getInstance() creates on first call, then reuses.
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
// same object reference
Watch out
Singletons couple code to a global. Tests get awkward. You can't swap a mock without hacks. Overuse kills modularity.
Often dependency injection or a plain service module gives you one instance without the pattern ceremony. Pass the logger in. Export a single createPool() result. Simpler to reason about.
When it fits
Limited resources, one config source, one error pipeline. Fine.
When requirements shift and you need two instances? Singleton fights you.
Reach for DI or services first. Use Singleton when you truly need lazy single creation and everyone understands the global. Type clarity and testability beat pattern bingo every time.