DEV Community

πŸ’» Arpad Kish πŸ’»
πŸ’» Arpad Kish πŸ’»

Posted on

Streamlining Dependency Injection in TypeScript: A Look at `singleton-factory-ts`

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:

  1. An abstract Singleton base class that your services will extend.
  2. 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();

Enter fullscreen mode Exit fullscreen mode

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.

https://github.com/arpad1337/singleton-factory-ts

Top comments (0)