Injection Types
All the ways to define and inject dependencies in NeoSyringe.
Overview
| Type | Syntax | Use 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.
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.
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>().
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:
// 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 sameICacheinterface 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:
// logger.ts
export interface ILogger { log(msg: string): void; }// 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 }],
});// 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:
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 === trueFactory Provider
Use factory functions when you need:
- Dynamic configuration
- Container access for conditional resolution
- External resource initialization
Arrow Function (Auto-detected)
{
token: useInterface<IConfig>(),
provider: () => ({
apiUrl: process.env.API_URL,
environment: process.env.NODE_ENV
})
}Factory with Container Access
{
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:
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.
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 instanceAlways 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:
// ❌ 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:
// ✅ 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?
// ❌ 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'):
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:
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:
const invalid = useProperty<string>(ApiService, 'invalidParam');
// 🔴 Error: Parameter 'invalidParam' does not exist in ApiService constructorMulti Registration
Register multiple providers for the same token. Resolve them all at once with resolveAll().
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
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 }
]
});