Streamlining Dependency Injection in TypeScript: A Look at `singleton-factory-ts`
Source: Dev.to
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(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 library’s developer experience is straightforward. Below is a typical definition of 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] {
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 access S2.instance, the base Singleton class intercepts the call and delegates to the SingletonFactory. The factory reads the Dependencies array, recursively builds (or retrieves) the required instances, and injects them into S2’s constructor.
Under the Hood: What Makes it Robust?
Smart Token Caching
Every class extending Singleton automatically receives an InjectorToken generated with Symbol.for(this.className). The SingletonFactory maintains a Map (_singletonCache) of these tokens. If an instance already exists, it returns the cached version, guaranteeing true singleton behavior across the application.
Circular Dependency Detection
Circular dependencies (e.g., Service A depends on Service B, which depends on Service A) can cause infinite loops. During instantiation, the factory adds the class’s InjectorToken to an _initializingClasses set. If it encounters a token already present in this set while resolving the dependency tree, it throws a descriptive error: Circular dependency detected for token….
Custom Instantiation via create Method
Sometimes the standard new Constructor(...) pattern isn’t sufficient—perhaps you need async operations or custom factory logic. The library includes a respondsToSelector polyfill (inspired by Objective‑C) on Object and Object.prototype. When the SingletonFactory resolves dependencies, it checks whether the class respondsToSelector("create"). If so, it calls Class.create!(...deps) instead of the constructor, providing great flexibility.
Real-World Adoption: @greeneyesai/api-utils
The pattern isn’t just experimental; it’s actively used in production. @greeneyesai/api-utils adopts a variant of this singleton‑factory architecture to manage its internal lifecycle and service dependencies. By leveraging the pattern, the library ensures that core clients, configuration managers, and logging services are instantiated lazily, share state reliably, and enforce dependency contracts without pulling in a heavyweight DI framework.
Conclusion
For developers aiming to decouple TypeScript classes while preserving strict type safety and modularity, singleton-factory-ts offers a concise blueprint. By combining a declarative static Dependencies array with a smart centralized factory, it delivers enterprise‑level DI capabilities in a lightweight, readable package.
Whether you import the library directly or adapt its architectural variant for tools like @greeneyesai/api-utils, the singleton factory pattern remains a powerful addition to any TypeScript developer’s toolkit.