Injector

Goal: Inject dependencies into constructor.

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

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

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 'ts-ioc-container';

/**
 * User Management Domain - Metadata Injection
 *
 * The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
 * to automatically inject dependencies into constructor parameters.
 *
 * How it works:
 * 1. @inject('key') decorator marks a parameter for injection
 * 2. Container reads metadata at resolution time
 * 3. Dependencies are resolved and passed to constructor
 *
 * This is the most common pattern in Angular, NestJS, and similar frameworks.
 * Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
 */

class Logger {
  name = 'Logger';
}

class App {
  // @inject tells the container which dependency to resolve for this parameter
  constructor(@inject('ILogger') private logger: Logger) {}

  // Alternative: inject via function for dynamic resolution
  // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}

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

describe('Metadata Injector', function () {
  it('should inject dependencies using @inject decorator', function () {
    const container = new Container({ tags: ['application'] }).addRegistration(
      R.fromClass(Logger).bindToKey('ILogger'),
    );

    // Container reads @inject metadata and resolves 'ILogger' for the logger parameter
    const app = container.resolve(App);

    expect(app.getLoggerName()).toBe('Logger');
  });
});
import { Container, inject, Registration as R } from 'ts-ioc-container';

/**
 * User Management Domain - Metadata Injection
 *
 * The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
 * to automatically inject dependencies into constructor parameters.
 *
 * How it works:
 * 1. @inject('key') decorator marks a parameter for injection
 * 2. Container reads metadata at resolution time
 * 3. Dependencies are resolved and passed to constructor
 *
 * This is the most common pattern in Angular, NestJS, and similar frameworks.
 * Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
 */

class Logger {
  name = 'Logger';
}

class App {
  // @inject tells the container which dependency to resolve for this parameter
  constructor(@inject('ILogger') private logger: Logger) {}

  // Alternative: inject via function for dynamic resolution
  // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}

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

describe('Metadata Injector', function () {
  it('should inject dependencies using @inject decorator', function () {
    const container = new Container({ tags: ['application'] }).addRegistration(
      R.fromClass(Logger).bindToKey('ILogger'),
    );

    // Container reads @inject metadata and resolves 'ILogger' for the logger parameter
    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.

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 'ts-ioc-container';

/**
 * Command Pattern - Simple Injector
 *
 * The SimpleInjector passes the container itself as the first argument to the constructor.
 * This is useful for:
 * - Service Locators (like Command Dispatchers or Routers)
 * - Factory classes that need to resolve dependencies dynamically
 * - Legacy code migration where passing the container is common
 *
 * In this example, a CommandDispatcher uses the container to dynamically
 * resolve the correct handler for each command type.
 */

interface ICommand {
  type: string;
}

interface ICommandHandler {
  handle(command: ICommand): string;
}

class CreateUserCommand implements ICommand {
  readonly type = 'CreateUser';
  constructor(readonly username: string) {}
}

class CreateUserHandler implements ICommandHandler {
  handle(command: CreateUserCommand): string {
    return `User ${command.username} created`;
  }
}

describe('SimpleInjector', function () {
  it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
    // Dispatcher needs the container to find handlers dynamically based on command type
    class CommandDispatcher {
      constructor(private container: IContainer) {}

      dispatch(command: ICommand): string {
        // Dynamically resolve handler: "Handler" + "CreateUser"
        const handlerKey = `Handler${command.type}`;
        const handler = this.container.resolve<ICommandHandler>(handlerKey);
        return handler.handle(command);
      }
    }

    const container = new Container({ injector: new SimpleInjector() })
      .addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
      .addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));

    const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
    const result = dispatcher.dispatch(new CreateUserCommand('alice'));

    expect(result).toBe('User alice created');
  });

  it('should pass additional arguments alongside the container', function () {
    // Factory that creates widgets with a specific theme
    class WidgetFactory {
      constructor(
        private container: IContainer,
        private theme: string, // Passed as argument during resolve
      ) {}

      createWidget(name: string): string {
        return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
      }
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
    );

    // Pass "dark" as the theme argument
    const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });

    expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
  });
});
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';

/**
 * Command Pattern - Simple Injector
 *
 * The SimpleInjector passes the container itself as the first argument to the constructor.
 * This is useful for:
 * - Service Locators (like Command Dispatchers or Routers)
 * - Factory classes that need to resolve dependencies dynamically
 * - Legacy code migration where passing the container is common
 *
 * In this example, a CommandDispatcher uses the container to dynamically
 * resolve the correct handler for each command type.
 */

interface ICommand {
  type: string;
}

interface ICommandHandler {
  handle(command: ICommand): string;
}

class CreateUserCommand implements ICommand {
  readonly type = 'CreateUser';
  constructor(readonly username: string) {}
}

class CreateUserHandler implements ICommandHandler {
  handle(command: CreateUserCommand): string {
    return `User ${command.username} created`;
  }
}

describe('SimpleInjector', function () {
  it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
    // Dispatcher needs the container to find handlers dynamically based on command type
    class CommandDispatcher {
      constructor(private container: IContainer) {}

      dispatch(command: ICommand): string {
        // Dynamically resolve handler: "Handler" + "CreateUser"
        const handlerKey = `Handler${command.type}`;
        const handler = this.container.resolve<ICommandHandler>(handlerKey);
        return handler.handle(command);
      }
    }

    const container = new Container({ injector: new SimpleInjector() })
      .addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
      .addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));

    const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
    const result = dispatcher.dispatch(new CreateUserCommand('alice'));

    expect(result).toBe('User alice created');
  });

  it('should pass additional arguments alongside the container', function () {
    // Factory that creates widgets with a specific theme
    class WidgetFactory {
      constructor(
        private container: IContainer,
        private theme: string, // Passed as argument during resolve
      ) {}

      createWidget(name: string): string {
        return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
      }
    }

    const container = new Container({ injector: new SimpleInjector() }).addRegistration(
      R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
    );

    // Pass "dark" as the theme argument
    const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });

    expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
  });
});

Use Cases

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 { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';

/**
 * Clean Architecture - Proxy Injector
 *
 * The ProxyInjector injects dependencies as a single object (props/options pattern).
 * This is popular in modern JavaScript/TypeScript (like React props or destructuring).
 *
 * Advantages:
 * - Named parameters are more readable than positional arguments
 * - Order of arguments doesn't matter
 * - Easy to add/remove dependencies without breaking inheritance chains
 * - Works well with "Clean Architecture" adapters
 */

describe('ProxyInjector', function () {
  it('should inject dependencies as a props object', function () {
    class Logger {
      log(msg: string) {
        return `Logged: ${msg}`;
      }
    }

    // Dependencies defined as an interface
    interface UserControllerDeps {
      logger: Logger;
      prefix: string;
    }

    // Controller receives all dependencies in a single object
    class UserController {
      private logger: Logger;
      private prefix: string;

      constructor({ logger, prefix }: UserControllerDeps) {
        this.logger = logger;
        this.prefix = prefix;
      }

      createUser(name: string): string {
        return this.logger.log(`${this.prefix} ${name}`);
      }
    }

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

    const controller = container.resolve<UserController>('UserController');

    expect(controller.createUser('bob')).toBe('Logged: USER: bob');
  });

  it('should support mixing injected dependencies with runtime arguments', function () {
    class Database {}

    interface ReportGeneratorDeps {
      database: Database;
      format: string; // Runtime argument
    }

    class ReportGenerator {
      constructor(public deps: ReportGeneratorDeps) {}

      generate(): string {
        return `Report in ${this.deps.format}`;
      }
    }

    const container = new Container({ injector: new ProxyInjector() })
      .addRegistration(R.fromClass(Database).bindToKey('database'))
      .addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));

    // "format" is passed at resolution time
    const generator = container.resolve<ReportGenerator>('ReportGenerator', {
      args: [{ format: 'PDF' }],
    });

    expect(generator.deps.database).toBeInstanceOf(Database);
    expect(generator.generate()).toBe('Report in PDF');
  });

  it('should resolve array dependencies by alias (convention over configuration)', function () {
    // If a property is named "loggersArray", it looks for alias "loggersArray"
    // and resolves it as an array of all matches.

    class FileLogger {}
    class ConsoleLogger {}

    interface AppDeps {
      loggersArray: any[]; // Injected as array of all loggers
    }

    class App {
      constructor(public deps: AppDeps) {}
    }

    const container = new Container({ injector: new ProxyInjector() });

    // Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
    // which delegates to the container.
    // In a real scenario, you'd register multiple loggers with the same alias.
    const mockLoggers = [new FileLogger(), new ConsoleLogger()];

    container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);

    const app = container.resolve(App);

    expect(app.deps.loggersArray).toBe(mockLoggers);
    expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
  });
});
import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';

/**
 * Clean Architecture - Proxy Injector
 *
 * The ProxyInjector injects dependencies as a single object (props/options pattern).
 * This is popular in modern JavaScript/TypeScript (like React props or destructuring).
 *
 * Advantages:
 * - Named parameters are more readable than positional arguments
 * - Order of arguments doesn't matter
 * - Easy to add/remove dependencies without breaking inheritance chains
 * - Works well with "Clean Architecture" adapters
 */

describe('ProxyInjector', function () {
  it('should inject dependencies as a props object', function () {
    class Logger {
      log(msg: string) {
        return `Logged: ${msg}`;
      }
    }

    // Dependencies defined as an interface
    interface UserControllerDeps {
      logger: Logger;
      prefix: string;
    }

    // Controller receives all dependencies in a single object
    class UserController {
      private logger: Logger;
      private prefix: string;

      constructor({ logger, prefix }: UserControllerDeps) {
        this.logger = logger;
        this.prefix = prefix;
      }

      createUser(name: string): string {
        return this.logger.log(`${this.prefix} ${name}`);
      }
    }

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

    const controller = container.resolve<UserController>('UserController');

    expect(controller.createUser('bob')).toBe('Logged: USER: bob');
  });

  it('should support mixing injected dependencies with runtime arguments', function () {
    class Database {}

    interface ReportGeneratorDeps {
      database: Database;
      format: string; // Runtime argument
    }

    class ReportGenerator {
      constructor(public deps: ReportGeneratorDeps) {}

      generate(): string {
        return `Report in ${this.deps.format}`;
      }
    }

    const container = new Container({ injector: new ProxyInjector() })
      .addRegistration(R.fromClass(Database).bindToKey('database'))
      .addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));

    // "format" is passed at resolution time
    const generator = container.resolve<ReportGenerator>('ReportGenerator', {
      args: [{ format: 'PDF' }],
    });

    expect(generator.deps.database).toBeInstanceOf(Database);
    expect(generator.generate()).toBe('Report in PDF');
  });

  it('should resolve array dependencies by alias (convention over configuration)', function () {
    // If a property is named "loggersArray", it looks for alias "loggersArray"
    // and resolves it as an array of all matches.

    class FileLogger {}
    class ConsoleLogger {}

    interface AppDeps {
      loggersArray: any[]; // Injected as array of all loggers
    }

    class App {
      constructor(public deps: AppDeps) {}
    }

    const container = new Container({ injector: new ProxyInjector() });

    // Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
    // which delegates to the container.
    // In a real scenario, you'd register multiple loggers with the same alias.
    const mockLoggers = [new FileLogger(), new ConsoleLogger()];

    container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);

    const app = container.resolve(App);

    expect(app.deps.loggersArray).toBe(mockLoggers);
    expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
  });
});

Use Cases

InjectorBest ForProsCons
MetadataInjectorMost applications, TypeScript projectsClean syntax, type-safe, IntelliSense supportRequires reflect-metadata, decorator support
SimpleInjectorManual control, legacy code, frameworksFull control, no decorators neededMore verbose, manual resolution
ProxyInjectorDestructuring patterns, functional styleExplicit parameter names, no decoratorsLess type inference, naming conventions

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

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, Registration } from 'ts-ioc-container';

/**
 * Advanced - Custom Injector
 *
 * You can implement your own injection strategy by extending the `Injector` class.
 * This is useful for integrating with other frameworks, supporting legacy patterns,
 * or implementing custom instantiation logic.
 *
 * Example: Static Factory Method Pattern
 * Some classes prefer to control their own instantiation via a static `create` method
 * rather than a public constructor. This custom injector supports that pattern.
 */

interface IFactoryClass<T> {
  create(container: IContainer, ...args: any[]): T;
}

class StaticFactoryInjector extends Injector {
  createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
    // Check if the class has a static 'create' method
    const factoryClass = target as unknown as IFactoryClass<T>;
    if (typeof factoryClass.create === 'function') {
      return factoryClass.create(container, ...args);
    }

    // Fallback to standard constructor instantiation
    return new target(...args);
  }
}

describe('Custom Injector', function () {
  it('should use static create method for instantiation when available', function () {
    class ApiClient {
      constructor(
        public baseUrl: string,
        public timeout: number,
      ) {}

      // Custom factory method
      static create(container: IContainer, config: { timeout: number }): ApiClient {
        const baseUrl = container.resolve<string>('BaseUrl');
        return new ApiClient(baseUrl, config.timeout);
      }
    }

    const container = new Container({ injector: new StaticFactoryInjector() })
      .addRegistration(Registration.fromValue('https://api.example.com').bindToKey('BaseUrl'))
      .addRegistration(Registration.fromClass(ApiClient));

    // Resolve using the custom injector which calls ApiClient.create
    const client = container.resolve(ApiClient, { args: [{ timeout: 5000 }] });

    expect(client.baseUrl).toBe('https://api.example.com');
    expect(client.timeout).toBe(5000);
  });
});
import { type constructor, Container, type IContainer, Injector, ProviderOptions, Registration } from 'ts-ioc-container';

/**
 * Advanced - Custom Injector
 *
 * You can implement your own injection strategy by extending the `Injector` class.
 * This is useful for integrating with other frameworks, supporting legacy patterns,
 * or implementing custom instantiation logic.
 *
 * Example: Static Factory Method Pattern
 * Some classes prefer to control their own instantiation via a static `create` method
 * rather than a public constructor. This custom injector supports that pattern.
 */

interface IFactoryClass<T> {
  create(container: IContainer, ...args: any[]): T;
}

class StaticFactoryInjector extends Injector {
  createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
    // Check if the class has a static 'create' method
    const factoryClass = target as unknown as IFactoryClass<T>;
    if (typeof factoryClass.create === 'function') {
      return factoryClass.create(container, ...args);
    }

    // Fallback to standard constructor instantiation
    return new target(...args);
  }
}

describe('Custom Injector', function () {
  it('should use static create method for instantiation when available', function () {
    class ApiClient {
      constructor(
        public baseUrl: string,
        public timeout: number,
      ) {}

      // Custom factory method
      static create(container: IContainer, config: { timeout: number }): ApiClient {
        const baseUrl = container.resolve<string>('BaseUrl');
        return new ApiClient(baseUrl, config.timeout);
      }
    }

    const container = new Container({ injector: new StaticFactoryInjector() })
      .addRegistration(Registration.fromValue('https://api.example.com').bindToKey('BaseUrl'))
      .addRegistration(Registration.fromClass(ApiClient));

    // Resolve using the custom injector which calls ApiClient.create
    const client = container.resolve(ApiClient, { args: [{ timeout: 5000 }] });

    expect(client.baseUrl).toBe('https://api.example.com');
    expect(client.timeout).toBe(5000);
  });
});

Extension Points

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