Skip to content

Injection Types

All the ways to define and inject dependencies in NeoSyringe.

Overview

TypeSyntaxUse Case
Class{ token: MyClass }Simple autowiring
Interface{ token: useInterface<IFoo>(), provider: Foo }Abstraction
Explicit{ token: MyClass, provider: OtherClass }Override default
Factory{ token: X, provider: (c) => ... }Dynamic creation
Value{ token: X, useValue: myValue }Pre-built objects
Property{ token: useProperty(Class, 'param') }Primitives
Multi{ token: X, provider: Y, multi: true }Plugin / strategy lists

Class Token

Register a class directly. Dependencies are resolved automatically from constructor parameters.

typescript
class Repository {
  findAll() { return []; }
}

class Service {
  constructor(private repo: Repository) {}
}

export const container = defineBuilderConfig({
  injections: [
    { token: Repository },
    { token: Service }  // Repository injected automatically
  ]
});

Interface Token

Bind an interface to a concrete implementation using useInterface<T>().

typescript
import { useInterface } from '@djodjonx/neosyringe';

interface ICache {
  get(key: string): any;
  set(key: string, value: any): void;
}

class RedisCache implements ICache {
  get(key: string) { /* ... */ }
  set(key: string, value: any) { /* ... */ }
}

class MemoryCache implements ICache {
  private store = new Map();
  get(key: string) { return this.store.get(key); }
  set(key: string, value: any) { this.store.set(key, value); }
}

// Production
const prodContainer = defineBuilderConfig({
  injections: [
    { token: useInterface<ICache>(), provider: RedisCache }
  ]
});

// Development
const devContainer = defineBuilderConfig({
  injections: [
    { token: useInterface<ICache>(), provider: MemoryCache }
  ]
});

How It Works

At compile-time, useInterface<ICache>() is replaced with a unique string ID:

typescript
// Before (your code)
{ token: useInterface<ICache>(), provider: RedisCache }

// After (generated)
{ token: "ICache", provider: RedisCache }

Explicit Provider

Override which class is used when resolving a token:

typescript
class UserService {
  validate(user: User) { /* production logic */ }
}

class TestUserService extends UserService {
  validate(user: User) { return true; } // Skip validation in tests
}

// Test container
const testContainer = defineBuilderConfig({
  injections: [
    { token: UserService, provider: TestUserService }
  ]
});

const service = testContainer.resolve(UserService);
// service instanceof TestUserService === true

Factory Provider

Use factory functions when you need:

  • Dynamic configuration
  • Container access for conditional resolution
  • External resource initialization

Arrow Function (Auto-detected)

typescript
{
  token: useInterface<IConfig>(),
  provider: () => ({
    apiUrl: process.env.API_URL,
    environment: process.env.NODE_ENV
  })
}

Factory with Container Access

typescript
{
  token: useInterface<IHttpClient>(),
  provider: (container) => {
    const config = container.resolve(useInterface<IConfig>());
    const logger = container.resolve(useInterface<ILogger>());
    
    return new HttpClient({
      baseUrl: config.apiUrl,
      logger,
      timeout: config.timeout
    });
  }
}

Explicit Factory Flag

For regular functions that aren't arrow functions:

typescript
function createDatabaseConnection(container: Container) {
  const config = container.resolve(useInterface<IDbConfig>());
  return new DatabaseConnection(config.connectionString);
}

{
  token: useInterface<IDatabase>(),
  provider: createDatabaseConnection,
  useFactory: true  // Required for non-arrow functions
}

Value Provider

Register a pre-built value directly — no class, no factory. The value is embedded in the generated container as-is.

typescript
interface DatabaseConfig {
  url: string;
  poolSize: number;
}

const dbConfig: DatabaseConfig = {
  url: process.env.DB_URL ?? 'localhost:5432',
  poolSize: 10
};

export const container = defineBuilderConfig({
  injections: [
    { token: useInterface<DatabaseConfig>(), useValue: dbConfig }
  ]
});

const config = container.resolve(useInterface<DatabaseConfig>());
// Returns the exact dbConfig object — always the same instance

Always Singleton

Values are always singletons. Each resolve() call returns the same object.

Primitives Not Supported

useValue does not accept primitive types (string, number, boolean). Use useProperty for injecting primitive configuration values into class constructors.

Property Token

Inject primitive values (string, number, boolean) into class constructors.

The Problem

How do you inject configuration values without polluting your classes?

typescript
// ❌ Coupled to DI
class ApiService {
  constructor(@inject('API_URL') private apiUrl: string) {}
}

// ❌ Coupled to environment
class ApiService {
  private apiUrl = process.env.API_URL;
}

The Solution

Use useProperty<T>(Class, 'paramName'):

typescript
import { useProperty } from '@djodjonx/neosyringe';

// ✅ Pure class
class ApiService {
  constructor(
    private apiUrl: string,
    private timeout: number,
    private retryCount: number
  ) {}
}

// Property tokens
const apiUrl = useProperty<string>(ApiService, 'apiUrl');
const timeout = useProperty<number>(ApiService, 'timeout');
const retryCount = useProperty<number>(ApiService, 'retryCount');

export const container = defineBuilderConfig({
  injections: [
    { token: apiUrl, provider: () => 'https://api.example.com' },
    { token: timeout, provider: () => 5000 },
    { token: retryCount, provider: () => 3 },
    { token: ApiService }  // All primitives injected!
  ]
});

Scoped to Class

Property tokens are scoped to their class, avoiding collisions:

typescript
const serviceAUrl = useProperty<string>(ServiceA, 'url');
const serviceBUrl = useProperty<string>(ServiceB, 'url');

// serviceAUrl !== serviceBUrl (different tokens!)

Validated by LSP

The IDE plugin validates property names:

typescript
const invalid = useProperty<string>(ApiService, 'invalidParam');
// 🔴 Error: Parameter 'invalidParam' does not exist in ApiService constructor

Multi Registration

Register multiple providers for the same token. Resolve them all at once with resolveAll().

typescript
import { defineBuilderConfig, useInterface } from '@djodjonx/neosyringe';

interface IPlugin {
  execute(): void;
}

class AuthPlugin implements IPlugin {
  execute() { /* check auth */ }
}

class LogPlugin implements IPlugin {
  execute() { /* log request */ }
}

class MetricsPlugin implements IPlugin {
  execute() { /* record metrics */ }
}

export const container = defineBuilderConfig({
  injections: [
    { token: useInterface<IPlugin>(), provider: AuthPlugin,    multi: true },
    { token: useInterface<IPlugin>(), provider: LogPlugin,     multi: true },
    { token: useInterface<IPlugin>(), provider: MetricsPlugin, multi: true },
  ]
});

// Resolve all registered providers
const plugins = container.resolveAll(useInterface<IPlugin>());
plugins.forEach(p => p.execute());

Use multi: true for plugin systems, middleware chains, and event handlers — anywhere you need an open-ended list of contributors for the same interface.

Order Preserved

Providers are resolved in registration order.

Cannot Mix

A token registered with multi: true cannot also be registered without it (and vice versa). NeoSyringe reports an error at build time.

Combined Example

typescript
import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neosyringe';

// Interfaces
interface ILogger { log(msg: string): void; }
interface IHttpClient { get(url: string): Promise<any>; }

// Implementations
class ConsoleLogger implements ILogger {
  log(msg: string) { console.log(msg); }
}

// Pure service class
class ApiService {
  constructor(
    private logger: ILogger,
    private http: IHttpClient,
    private baseUrl: string,
    private timeout: number
  ) {}
}

// Tokens
const baseUrl = useProperty<string>(ApiService, 'baseUrl');
const timeout = useProperty<number>(ApiService, 'timeout');

export const container = defineBuilderConfig({
  name: 'AppContainer',
  injections: [
    // Interface bindings
    { token: useInterface<ILogger>(), provider: ConsoleLogger },
    
    // Factory for complex initialization
    {
      token: useInterface<IHttpClient>(),
      provider: (container) => {
        const logger = container.resolve(useInterface<ILogger>());
        return new HttpClient(logger);
      }
    },
    
    // Primitive values
    { token: baseUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
    { token: timeout, provider: () => 5000 },
    
    // Service with mixed dependencies
    { token: ApiService }
  ]
});

Released under the MIT License.