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 { bindTo, Container, inject, register, 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.
 */

@register(bindTo('ILogger'))
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));

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

    expect(app.getLoggerName()).toBe('Logger');
  });
});
import { bindTo, Container, inject, register, 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.
 */

@register(bindTo('ILogger'))
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));

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

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

args(index) and argsFn — Positional Arg Injection

args(index) and argsFn(fn) are thin shortcuts for an InjectFn. Every InjectFn receives (scope, options) as parameters, where options.args holds the runtime args array passed via ProviderOptions:

class Service {
  constructor(
    // equivalent to args(0)
    @inject((scope, { args = [] }) => args[0]) value: string,

    // equivalent to argsFn((a, b) => (a as number) + (b as number))
    @inject((scope, { args = [] }) => (args[0] as number) + (args[1] as number)) sum: number,
  ) {}
}

Use them with @inject when you want a constructor parameter to receive a positional argument from the resolution context rather than a registered dependency.

Constructor parameters that should pick up positional args from ProviderOptions must be annotated with @inject(args(index)). Parameters without @inject resolve to undefined. InjectionToken values passed in args are resolved automatically before they reach the constructor.

TypeScript __tests__/injector/inject.spec.ts
import 'reflect-metadata';
import {
  args,
  appendArgs,
  appendArgsFn,
  argsFn,
  Container,
  inject,
  register,
  Registration as R,
  SingleToken,
} from 'ts-ioc-container';

describe('inject helpers', () => {
  function createContainer() {
    return new Container();
  }

  describe('args(index)', () => {
    it('resolves InjectionToken args before reaching @inject(args(...))', () => {
      const ValueToken = new SingleToken<string>('value');

      class Service {
        constructor(@inject(args(0)) public value: string) {}
      }

      const ServiceToken = new SingleToken<Service>('Service');
      const container = createContainer()
        .addRegistration(R.fromValue('injected').bindTo(ValueToken))
        .addRegistration(R.fromClass(Service).bindTo(ServiceToken));

      const instance = ServiceToken.args(ValueToken).resolve(container);
      expect(instance.value).toBe('injected');
    });

    it('returns undefined for out-of-bounds index', () => {
      @register(appendArgs('only'))
      class Service {
        constructor(@inject(args(5)) public value: unknown) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').value).toBeUndefined();
    });
  });

  describe('argsFn', () => {
    it('receives an empty array when no args are provided', () => {
      class Service {
        constructor(@inject(argsFn((...a) => a.length)) public count: number) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').count).toBe(0);
    });

    it('can transform args into a complex object', () => {
      @register(appendArgsFn(() => ['x', 'y']))
      class Service {
        constructor(@inject(argsFn((a, b) => ({ first: a, second: b }))) public data: unknown) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').data).toEqual({ first: 'x', second: 'y' });
    });
  });
});
import 'reflect-metadata';
import {
  args,
  appendArgs,
  appendArgsFn,
  argsFn,
  Container,
  inject,
  register,
  Registration as R,
  SingleToken,
} from 'ts-ioc-container';

describe('inject helpers', () => {
  function createContainer() {
    return new Container();
  }

  describe('args(index)', () => {
    it('resolves InjectionToken args before reaching @inject(args(...))', () => {
      const ValueToken = new SingleToken<string>('value');

      class Service {
        constructor(@inject(args(0)) public value: string) {}
      }

      const ServiceToken = new SingleToken<Service>('Service');
      const container = createContainer()
        .addRegistration(R.fromValue('injected').bindTo(ValueToken))
        .addRegistration(R.fromClass(Service).bindTo(ServiceToken));

      const instance = ServiceToken.args(ValueToken).resolve(container);
      expect(instance.value).toBe('injected');
    });

    it('returns undefined for out-of-bounds index', () => {
      @register(appendArgs('only'))
      class Service {
        constructor(@inject(args(5)) public value: unknown) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').value).toBeUndefined();
    });
  });

  describe('argsFn', () => {
    it('receives an empty array when no args are provided', () => {
      class Service {
        constructor(@inject(argsFn((...a) => a.length)) public count: number) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').count).toBe(0);
    });

    it('can transform args into a complex object', () => {
      @register(appendArgsFn(() => ['x', 'y']))
      class Service {
        constructor(@inject(argsFn((a, b) => ({ first: a, second: b }))) public data: unknown) {}
      }

      const container = createContainer().addRegistration(R.fromClass(Service));
      expect(container.resolve<Service>('Service').data).toEqual({ first: 'x', second: 'y' });
    });
  });
});

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__/readme/simpleInjector.spec.ts
import { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';

@register(bindTo('HandlerCreateUser'))
class CreateUserHandler {
  handle(username: string): string {
    return `User ${username} created`;
  }
}

describe('SimpleInjector', function () {
  it('should inject container to allow dynamic resolution', function () {
    @register(bindTo('Dispatcher'))
    class CommandDispatcher {
      constructor(private container: IContainer) {}

      dispatch(type: string, payload: string): string {
        const handler = this.container.resolve<CreateUserHandler>(`Handler${type}`);
        return handler.handle(payload);
      }
    }

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

    const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');

    expect(dispatcher.dispatch('CreateUser', 'alice')).toBe('User alice created');
  });

  it('should pass additional arguments alongside the container', function () {
    class WidgetFactory {
      constructor(
        private container: IContainer,
        private theme: string,
      ) {}

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

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

    const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });

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

@register(bindTo('HandlerCreateUser'))
class CreateUserHandler {
  handle(username: string): string {
    return `User ${username} created`;
  }
}

describe('SimpleInjector', function () {
  it('should inject container to allow dynamic resolution', function () {
    @register(bindTo('Dispatcher'))
    class CommandDispatcher {
      constructor(private container: IContainer) {}

      dispatch(type: string, payload: string): string {
        const handler = this.container.resolve<CreateUserHandler>(`Handler${type}`);
        return handler.handle(payload);
      }
    }

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

    const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');

    expect(dispatcher.dispatch('CreateUser', 'alice')).toBe('User alice created');
  });

  it('should pass additional arguments alongside the container', function () {
    class WidgetFactory {
      constructor(
        private container: IContainer,
        private theme: string,
      ) {}

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

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

    const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });

    expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (container: 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.

[!WARNING]

ProxyInjector resolves dependencies from property names. Renaming, minification, or unclear alias naming can change resolution behavior.

Reserved keywords and conventions

TypeScript __tests__/readme/proxyInjector.spec.ts
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';

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

    class UserController {
      private logger: Logger;
      private prefix: string;

      constructor({ logger, prefix }: { logger: Logger; prefix: string }) {
        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))
      .addRegistration(R.fromValue('USER:').bindToKey('prefix'))
      .addRegistration(R.fromClass(UserController));

    expect(container.resolve<UserController>('UserController').createUser('bob')).toBe('Logged: USER: bob');
  });

  it('should expose runtime args through the reserved "args" property', function () {
    class ReportGenerator {
      format: string;

      constructor({ args }: { args: string[] }) {
        this.format = args[0];
      }

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

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

    const generator = container.resolve<ReportGenerator>('ReportGenerator', { args: ['PDF'] });

    expect(generator.generate()).toBe('Report in PDF');
  });
});
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';

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

    class UserController {
      private logger: Logger;
      private prefix: string;

      constructor({ logger, prefix }: { logger: Logger; prefix: string }) {
        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))
      .addRegistration(R.fromValue('USER:').bindToKey('prefix'))
      .addRegistration(R.fromClass(UserController));

    expect(container.resolve<UserController>('UserController').createUser('bob')).toBe('Logged: USER: bob');
  });

  it('should expose runtime args through the reserved "args" property', function () {
    class ReportGenerator {
      format: string;

      constructor({ args }: { args: string[] }) {
        this.format = args[0];
      }

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

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

    const generator = container.resolve<ReportGenerator>('ReportGenerator', { args: ['PDF'] });

    expect(generator.generate()).toBe('Report in PDF');
  });
});

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: