typescriptadvanced

Dependency Injection Container

Build a lightweight DI container with singleton and transient scopes for testable Node.js applications.

typescript
type Factory<T> = () => T;

class Container {
  private singletons = new Map<string, unknown>();
  private factories = new Map<string, { factory: Factory<unknown>; singleton: boolean }>();

  register<T>(token: string, factory: Factory<T>, options?: { singleton?: boolean }): this {
    this.factories.set(token, { factory, singleton: options?.singleton ?? true });
    return this;
  }

  resolve<T>(token: string): T {
    const entry = this.factories.get(token);
    if (!entry) throw new Error(`No registration for: ${token}`);

    if (entry.singleton) {
      if (!this.singletons.has(token)) {
        this.singletons.set(token, entry.factory());
      }
      return this.singletons.get(token) as T;
    }
    return entry.factory() as T;
  }

  // Create child container for scoped overrides (testing)
  createChild(): Container {
    const child = new Container();
    for (const [token, entry] of this.factories) {
      child.factories.set(token, entry);
    }
    return child;
  }
}

// Interfaces
interface Logger { info(msg: string): void; error(msg: string): void; }
interface Database { query(sql: string): Promise<unknown[]>; }
interface UserService { findUser(id: string): Promise<{ id: string; name: string } | null>; }

// Implementations
const container = new Container()
  .register<Logger>('Logger', () => ({
    info: (msg) => console.log(`[INFO] ${msg}`),
    error: (msg) => console.error(`[ERROR] ${msg}`),
  }))
  .register<Database>('Database', () => ({
    query: async (sql) => {
      console.log(`Executing: ${sql}`);
      return [{ id: '1', name: 'Alice' }];
    },
  }))
  .register<UserService>('UserService', () => {
    const db = container.resolve<Database>('Database');
    const logger = container.resolve<Logger>('Logger');
    return {
      findUser: async (id) => {
        logger.info(`Finding user: ${id}`);
        const rows = await db.query(`SELECT * FROM users WHERE id = '${id}'`);
        return rows[0] as { id: string; name: string } | null;
      },
    };
  });

// Usage
async function main() {
  const userService = container.resolve<UserService>('UserService');
  const user = await userService.findUser('1');
  console.log('Found:', user);

  // Testing: override with mocks
  const testContainer = container.createChild();
  testContainer.register<Database>('Database', () => ({
    query: async () => [{ id: 'mock', name: 'Mock User' }],
  }));
}

main();

Use Cases

  • Testable application architecture
  • Service layer composition
  • Mock injection for testing

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.