Injector

The injector determines how dependencies are passed to constructors. The container supports three injection strategies, each suited for different use cases and coding styles.

Injection Strategies

The container supports three injection strategies, each implementing the IInjector interface:

graph TB IInjector["IInjector Interface"] Metadata["MetadataInjector
Uses @inject decorators
+ reflect-metadata"] Simple["SimpleInjector
Passes container
as first arg"] Proxy["ProxyInjector
Matches param names
to dependency keys"] IInjector -->|implements| Metadata IInjector -->|implements| Simple IInjector -->|implements| Proxy Metadata -->|injects| Instance1["Instance with
decorated params"] Simple -->|injects| Instance2["Instance with
container param"] Proxy -->|injects| Instance3["Instance with
destructured params"] style IInjector fill:#0366d6,color:#fff style Metadata fill:#28a745,color:#fff style Simple fill:#ffc107,color:#000 style Proxy fill:#17a2b8,color:#fff

Available Injectors

  • MetadataInjector (default): Uses @inject decorators with reflection metadata
  • ProxyInjector: Injects dependencies as a dictionary object, perfect for destructuring
  • SimpleInjector: Passes the container as the first constructor argument for manual resolution

Metadata Injector

The default injector uses TypeScript decorators and reflection metadata to inject dependencies. This provides a clean, declarative syntax that's easy to read and maintain.

Basic Usage

TypeScript __tests__/readme/metadataInjector.spec.ts
import { Container, inject, Registration as R } from '../../lib';

class Logger {
  name = 'Logger';
}

class App {
  constructor(@inject('ILogger') private logger: Logger) {}

  // OR
  // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {
  // }

  getLoggerName(): string {
    return this.logger.name;
  }
}

describe('Reflection Injector', function () {
  it('should inject dependencies by @inject decorator', function () {
    const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));

    const app = container.resolve(App);

    expect(app.getLoggerName()).toBe('Logger');
  });
});
import { Container, inject, Registration as R } from '../../lib';

class Logger {
  name = 'Logger';
}

class App {
  constructor(@inject('ILogger') private logger: Logger) {}

  // OR
  // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {
  // }

  getLoggerName(): string {
    return this.logger.name;
  }
}

describe('Reflection Injector', function () {
  it('should inject dependencies by @inject decorator', function () {
    const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));

    const app = container.resolve(App);

    expect(app.getLoggerName()).toBe('Logger');
  });
});

Property Injection

The Metadata Injector also supports property injection using hooks. This is useful when constructor injection isn't possible or when you need to inject into base class properties. See the Hooks chapter for detailed information about property injection.

Simple Injector

The Simple Injector passes the container instance as the first constructor parameter, giving you direct access to resolve dependencies manually. This approach is useful when you need more control over dependency resolution.

TypeScript __tests__/injector/SimpleInjector.spec.ts
import { Container, type IContainer, Registration as R, SimpleInjector } from '../../lib';

describe('SimpleInjector', function () {
  it('should pass container as first parameter', function () {
    class App {
      constructor(public container: IContainer) {}
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(App).bindToKey('App'),
    );
    const app = container.resolve<App>('App');

    expect(app.container).toBeInstanceOf(Container);
  });

  it('should pass parameters alongside with container', function () {
    class App {
      constructor(
        container: IContainer,
        public greeting: string,
      ) {}
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(App).bindToKey('App'),
    );
    const app = container.resolve<App>('App', { args: ['Hello world'] });

    expect(app.greeting).toBe('Hello world');
  });
});
import { Container, type IContainer, Registration as R, SimpleInjector } from '../../lib';

describe('SimpleInjector', function () {
  it('should pass container as first parameter', function () {
    class App {
      constructor(public container: IContainer) {}
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(App).bindToKey('App'),
    );
    const app = container.resolve<App>('App');

    expect(app.container).toBeInstanceOf(Container);
  });

  it('should pass parameters alongside with container', function () {
    class App {
      constructor(
        container: IContainer,
        public greeting: string,
      ) {}
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(App).bindToKey('App'),
    );
    const app = container.resolve<App>('App', { args: ['Hello world'] });

    expect(app.greeting).toBe('Hello world');
  });
});

Use Cases

  • Manual dependency resolution
  • Dynamic dependency selection
  • Legacy code integration
  • Framework integration where container access is needed

Proxy Injector

The Proxy Injector matches constructor parameter names to dependency keys and injects them as a dictionary object. This is useful for object destructuring patterns and functional programming styles.

TypeScript __tests__/injector/ProxyInjector.spec.ts
import { args, Container, ProxyInjector, Registration as R } from '../../lib';

describe('ProxyInjector', function () {
  it('should pass dependency to constructor as dictionary', function () {
    class Logger {}

    class App {
      logger: Logger;

      constructor({ logger }: { logger: Logger }) {
        this.logger = logger;
      }
    }

    const container = new Container({ injector: new ProxyInjector() }).addRegistration(
      R.fromClass(Logger).bindToKey('logger'),
    );

    const app = container.resolve(App);
    expect(app.logger).toBeInstanceOf(Logger);
  });

  it('should pass arguments as objects', function () {
    class Logger {}

    class App {
      logger: Logger;
      greeting: string;

      constructor({
        logger,
        greetingTemplate,
        name,
      }: {
        logger: Logger;
        greetingTemplate: (name: string) => string;
        name: string;
      }) {
        this.logger = logger;
        this.greeting = greetingTemplate(name);
      }
    }

    const greetingTemplate = (name: string) => `Hello ${name}`;

    const container = new Container({ injector: new ProxyInjector() })
      .addRegistration(R.fromClass(App).bindToKey('App').pipe(args({ greetingTemplate })))
      .addRegistration(R.fromClass(Logger).bindToKey('logger'));

    const app = container.resolve<App>('App', { args: [{ name: `world` }] });
    expect(app.greeting).toBe('Hello world');
  });

  it('should resolve array dependencies when property name contains "array"', function () {
    class Logger {}
    class Service {}

    class App {
      loggers: Logger[];
      service: Service;

      constructor({ loggersArray, service }: { loggersArray: Logger[]; service: Service }) {
        this.loggers = loggersArray;
        this.service = service;
      }
    }

    // Mock container's resolveByAlias to return an array with a Logger instance
    const mockLogger = new Logger();
    const mockContainer = new Container({ injector: new ProxyInjector() });
    mockContainer.resolveByAlias = jest.fn().mockImplementation((key) => {
      // Always return the mock array for simplicity
      return [mockLogger];
    });
    mockContainer.addRegistration(R.fromClass(Service).bindToKey('service'));

    const app = mockContainer.resolve(App);
    expect(app.loggers).toBeInstanceOf(Array);
    expect(app.loggers.length).toBe(1);
    expect(app.loggers[0]).toBe(mockLogger);
    expect(app.service).toBeInstanceOf(Service);
    // Verify that resolveByAlias was called with the correct key
    expect(mockContainer.resolveByAlias).toHaveBeenCalledWith('loggersArray');
  });
});
import { args, Container, ProxyInjector, Registration as R } from '../../lib';

describe('ProxyInjector', function () {
  it('should pass dependency to constructor as dictionary', function () {
    class Logger {}

    class App {
      logger: Logger;

      constructor({ logger }: { logger: Logger }) {
        this.logger = logger;
      }
    }

    const container = new Container({ injector: new ProxyInjector() }).addRegistration(
      R.fromClass(Logger).bindToKey('logger'),
    );

    const app = container.resolve(App);
    expect(app.logger).toBeInstanceOf(Logger);
  });

  it('should pass arguments as objects', function () {
    class Logger {}

    class App {
      logger: Logger;
      greeting: string;

      constructor({
        logger,
        greetingTemplate,
        name,
      }: {
        logger: Logger;
        greetingTemplate: (name: string) => string;
        name: string;
      }) {
        this.logger = logger;
        this.greeting = greetingTemplate(name);
      }
    }

    const greetingTemplate = (name: string) => `Hello ${name}`;

    const container = new Container({ injector: new ProxyInjector() })
      .addRegistration(R.fromClass(App).bindToKey('App').pipe(args({ greetingTemplate })))
      .addRegistration(R.fromClass(Logger).bindToKey('logger'));

    const app = container.resolve<App>('App', { args: [{ name: `world` }] });
    expect(app.greeting).toBe('Hello world');
  });

  it('should resolve array dependencies when property name contains "array"', function () {
    class Logger {}
    class Service {}

    class App {
      loggers: Logger[];
      service: Service;

      constructor({ loggersArray, service }: { loggersArray: Logger[]; service: Service }) {
        this.loggers = loggersArray;
        this.service = service;
      }
    }

    // Mock container's resolveByAlias to return an array with a Logger instance
    const mockLogger = new Logger();
    const mockContainer = new Container({ injector: new ProxyInjector() });
    mockContainer.resolveByAlias = jest.fn().mockImplementation((key) => {
      // Always return the mock array for simplicity
      return [mockLogger];
    });
    mockContainer.addRegistration(R.fromClass(Service).bindToKey('service'));

    const app = mockContainer.resolve(App);
    expect(app.loggers).toBeInstanceOf(Array);
    expect(app.loggers.length).toBe(1);
    expect(app.loggers[0]).toBe(mockLogger);
    expect(app.service).toBeInstanceOf(Service);
    // Verify that resolveByAlias was called with the correct key
    expect(mockContainer.resolveByAlias).toHaveBeenCalledWith('loggersArray');
  });
});

Use Cases

  • Object destructuring in constructors
  • Named parameter patterns
  • Functional programming styles
  • When you prefer explicit parameter names over decorators

Choosing the Right Injector

Injector Best For Pros Cons
MetadataInjector Most applications, TypeScript projects Clean syntax, type-safe, IntelliSense support Requires reflect-metadata, decorator support
SimpleInjector Manual control, legacy code, frameworks Full control, no decorators needed More verbose, manual resolution
ProxyInjector Destructuring patterns, functional style Explicit parameter names, no decorators Less type inference, naming conventions

Strategy Pattern

The IInjector interface uses the Strategy pattern, allowing different injection strategies to be used interchangeably. This makes the system flexible and extensible.

Custom Injectors

You can create custom injectors by implementing the Injector interface. This allows you to implement injection strategies tailored to your specific needs.

TypeScript __tests__/readme/customInjector.spec.ts
import { type constructor, Container, type IContainer, Injector, ProviderOptions } from '../../lib';

class CustomInjector extends Injector {
  createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
    return new App(args[0] as string) as T;
  }
}

class App {
  constructor(public version: string) {}
}

describe('Custom Injector', function () {
  it('should use custom injector for dependency injection', function () {
    const container = new Container({ injector: new CustomInjector() });

    const app = container.resolve<App>(App, { args: ['1.0.0'] });

    expect(app.version).toBe('1.0.0');
  });
});
import { type constructor, Container, type IContainer, Injector, ProviderOptions } from '../../lib';

class CustomInjector extends Injector {
  createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
    return new App(args[0] as string) as T;
  }
}

class App {
  constructor(public version: string) {}
}

describe('Custom Injector', function () {
  it('should use custom injector for dependency injection', function () {
    const container = new Container({ injector: new CustomInjector() });

    const app = container.resolve<App>(App, { args: ['1.0.0'] });

    expect(app.version).toBe('1.0.0');
  });
});

Extension Points

Custom injectors are one of the key extension points in the architecture. They allow you to:

  • Implement framework-specific injection patterns
  • Add custom parameter resolution logic
  • Integrate with legacy codebases
  • Support alternative dependency injection styles

Best Practices

  • Use MetadataInjector by default - It provides the best developer experience for most TypeScript projects
  • Choose SimpleInjector for manual control - When you need explicit control over dependency resolution
  • Use ProxyInjector for destructuring - When you prefer object destructuring patterns
  • Be consistent - Stick with one injector strategy throughout your application for maintainability