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
  ]
});

Optional Dependencies ​

A constructor parameter marked ? (or typed T | undefined) is optional: if nothing registers it anywhere (this container, a useContainer parent, or an extends-ed partial), it resolves to undefined instead of failing the build. If it is registered somewhere, it's wired normally.

typescript
interface ITelemetry { track(event: string): void; }

class UserService {
  // Optional — no build error even if ITelemetry is never registered
  constructor(private telemetry?: ITelemetry) {}

  createUser(name: string) {
    this.telemetry?.track('user_created'); // guard the call yourself
    // ...
  }
}

export const container = defineBuilderConfig({
  injections: [{ token: UserService }]  // no ITelemetry registration — this is fine
});

There's no new field to learn — this reuses ordinary TypeScript syntax rather than adding a DI-specific annotation. Use it for genuinely optional collaborators (telemetry, an optional plugin) where a missing registration is expected, not a mistake. For anything that's actually required, leave the parameter non-optional so a missing registration still fails the build loudly.

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_a1b2c3d4", provider: RedisCache }

Token Identity & Scope ​

The token ID is derived from where the interface type is declared — its symbol and declaring source file — not from where useInterface<T>() is called. This means:

  • Calling useInterface<ICache>() for the same ICache interface in three different files produces the exact same token in all three.
  • You can register a token in one file and resolve it (directly, or via an auto-wired class's typed constructor parameter) from a completely different file:
typescript
// logger.ts
export interface ILogger { log(msg: string): void; }
typescript
// parent-root.ts
import { defineBuilderConfig, useInterface } from '@djodjonx/neosyringe';
import { ILogger } from './logger';
import { ConsoleLogger } from './console-logger';

export const parentContainer = defineBuilderConfig({
  injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }],
});
typescript
// child-root.ts — a different file, same ILogger type
import { defineBuilderConfig } from '@djodjonx/neosyringe';
import { parentContainer } from './parent-root';
import type { ILogger } from './logger';

export class ConsumerA {
  constructor(private readonly logger: ILogger) {}
}

export const childContainer = defineBuilderConfig({
  useContainer: parentContainer,
  injections: [{ token: ConsumerA }],
});

// ConsumerA's ILogger dependency resolves to the parent's ConsoleLogger instance —
// same token, because it's the same declared interface.

Recommended convention, not a technical requirement

Nothing requires useInterface<T>() and defineBuilderConfig calls to live in one file per module — the token identity works across files by design (see above). Confining a module's registrations to one composition-root file is a recommended convention for readability (one obvious place to see everything a module wires up), not something the compiler enforces.

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.

Class Tokens Not Supported

useValue only works with interface tokens (useInterface<T>()) and property tokens (useProperty<T>()) — both are resolved by string comparison. A class-constructor token ({ token: SomeClass }) is resolved by class identity, which a pre-built value has no way to satisfy:

typescript
// ❌ Throws TypeMismatchError at build time
{ token: SomeConcreteClass, useValue: someInstance }

For a pre-built instance registered under a class token (e.g. a class constructed with arguments the container can't itself supply), use a factory instead:

typescript
// ✅ Correct
{ token: SomeConcreteClass, provider: () => someInstance, useFactory: true }

See TypeMismatchError reference for the exact error text.

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.