Managing dependencies and object lifecycles in large TypeScript applications can often lead to boilerplate-heavy configurations and tangled initialization code. While massive frameworks like NestJS or Angular provide robust Dependency Injection (DI) containers out of the box, sometimes you need a lightweight, elegant solution without the overhead.
Enter singleton-factory-ts, an open-source library authored by @arpad1337 under the MIT License. This neat tool offers a streamlined approach to managing singletons and their dependencies using a highly intuitive class-based architecture.
In this article, we'll dive into how this pattern works, what makes it special, and how real-world toolsβlike @greeneyesai/api-utilsβare leveraging variations of it.
The Core Concept: Singleton and SingletonFactory
At the heart of singleton-factory-ts are two main components:
- An abstract
Singletonbase class that your services will extend. - A
SingletonFactory(which is itself a singleton) responsible for resolving dependencies, managing the cache, and detecting architectural flaws like circular dependencies.
Instead of registering services manually in a container, classes declare their own dependencies using a static getter.
Clean, Declarative Usage
The beauty of this library lies in its developer experience. Here is how you define singletons and their relationships:
import { Singleton, SingletonClassType } from 'singleton-factory-ts';
// A base singleton with no dependencies
class S1 extends Singleton {
doSomething() {
console.log("S1 is working!");
}
}
// A singleton that depends on S1
class S2 extends Singleton {
// 1. Declare dependencies declaratively
static get Dependencies(): [SingletonClassType<S1>] {
return [S1];
}
// 2. The factory will automatically inject S1 here
constructor(protected _s1: S1) {
super();
}
execute() {
this._s1.doSomething();
}
}
// 3. Access the instance effortlessly
const s2 = S2.instance;
s2.execute();
When you call S2.instance, the base Singleton class intercepts the call and defers to the SingletonFactory. The factory then looks at the Dependencies array, recursively builds (or retrieves) the required instances, and injects them into S2's constructor.
Under the Hood: What Makes it Robust?
While the API is simple, the internal mechanics of singleton-factory-ts solve several common DI headaches automatically.
1. Smart Token Caching
Every class extending Singleton automatically receives an InjectorToken generated using Symbol.for(this.className). The SingletonFactory maintains a Map (_singletonCache) of these tokens. If an instance already exists, it instantly returns the cached version, ensuring true singleton behavior across your application.
2. Circular Dependency Detection
One of the most frustrating errors in DI architectures is the infinite loop caused by circular dependencies (e.g., Service A depends on Service B, which depends on Service A). singleton-factory-ts handles this gracefully.
During instantiation, it adds the class's InjectorToken to an _initializingClasses Set. If it encounters a token that is already in this Set while trying to resolve the dependency tree, it immediately throws a descriptive error: Circular dependency detected for token....
3. Custom Instantiation via create Method
Sometimes, the standard new Constructor(...) pattern isn't enoughβyou might need to run async operations or specific factory logic.
To support this, the library includes a neat respondsToSelector polyfill patched onto the global Object and Object.prototype (inspired by Objective-C). When the SingletonFactory resolves dependencies, it checks if the class respondsToSelector("create"). If it does, it calls Class.create!(...deps) instead of the standard constructor, allowing for immense flexibility.
Real-World Adoption: @greeneyesai/api-utils
The elegant simplicity of this pattern makes it highly adaptable. It isn't just an experimental concept; it is actively shaping how utility libraries are built.
A prime example is @greeneyesai/api-utils, which uses a variant of this exact singleton-factory architecture to manage its internal lifecycle and service dependencies. By utilizing this pattern, @greeneyesai/api-utils can ensure that its core clients, configuration managers, and logging services are instantiated lazily, share state reliably, and strictly enforce dependency contracts without dragging in a heavy third-party DI framework.
Conclusion
For developers looking to decouple their TypeScript classes while maintaining strict type safety and modularity, singleton-factory-ts offers a brilliant blueprint. By mixing a declarative static Dependencies array with a smart centralized factory, it brings enterprise-level DI capabilities into a highly readable, lightweight package.
Whether you import the library directly or adopt its architectural variant for your own bespoke tools like @greeneyesai/api-utils, the singleton factory pattern remains a powerful weapon in the TypeScript developer's arsenal.
Top comments (0)