Providers
A provider binds a token to the value it produces. The providers array on @Injectable is a registry of these bindings, resolved on demand when a constructor parameter requests a token via @Inject. Resolution is by token, so the order of entries is irrelevant.
A token is any stable key: a class, an abstract class, an InjectionToken, or a plain string / symbol. xInject supports four provider forms.
Bare class (self-provided)
A class registered with @Injectable is its own token. Requesting it with @Inject resolves it directly - no entry in providers is required, so this is the common case for class dependencies.
@Injectable()
class UserService {
constructor(@Inject(Database) public db: Database) {}
}Listing a bare class in providers is shorthand for { provide: TheClass, useClass: TheClass }.
useClass
Bind a token to a different class - swapping or redirecting an implementation. The token resolves to useClass instead of itself; that class is resolved through inject, so it carries its own metadata and dependencies.
@Injectable({ providers: [{ provide: Logger, useClass: FileLogger }] })
class ReportService {
constructor(@Inject(Logger) public logger: Logger) {} // gets a FileLogger
}useFactory
Produce the value by calling a function. Its return value becomes the dependency; the tokens listed in deps are resolved and passed to it as arguments, in order.
{ provide: CACHE, useFactory: (config) => createCacheClient(config), deps: [ REDIS_CONFIG ] }useValue
Bind a token to a fixed, pre-built value - a config object, constant, or ready-made instance. It is provided as-is, with no instantiation or resolution.
{ provide: API_CONFIG, useValue: { apiKey: 'sk-xxx', baseUrl: 'https://api.example.com' } }Putting it together
import { Injectable, InjectionToken, ProvidersType } from '@remotex-labs/xinject';
const ENV = new InjectionToken<'development' | 'production'>('ENV');
const CACHE = new InjectionToken<CacheClient>('CACHE');
const providers: ProvidersType = [
UserService, // bare class (self-provided)
{ provide: Logger, useClass: FileLogger }, // useClass
{ provide: CACHE, useFactory: createCacheClient, deps: [ ENV ] },// useFactory
{ provide: ENV, useValue: 'development' as const } // useValue
];Factory dependencies
useFactory declares its own dependencies with deps, a list of tokens resolved from the same registry before the factory runs. This lets a factory compose other providers:
const CONFIG = new InjectionToken<{ n: number }>('CONFIG');
const RESULT = new InjectionToken<number>('RESULT');
@Injectable({
providers: [
{ provide: CONFIG, useValue: { n: 41 } },
{ provide: RESULT, useFactory: (config: { n: number }) => config.n + 1, deps: [ CONFIG ] }
]
})
class Service {
constructor(@Inject(RESULT) public result: number) {} // 42
}Circular dependencies
xInject tracks the resolution path and throws when it detects a cycle, rather than overflowing the stack. The error names the chain:
// A depends on B, B depends on A
inject(A); // throws: Circular dependency detected: A -> B -> ADetection carries no module-level state - the path is threaded through each resolution and copied at every hop, so independent branches (a diamond, where two dependencies share a leaf) are never mistaken for a cycle.
Mutual class cycles
Because a class named in @Inject(Other) must already be defined, two classes that inject each other cannot be written directly today - a forwardRef escape hatch is not yet provided. Cycles through useFactory deps are detected as shown above.
Type guards
isProviderUseClass, isProviderUseValue, and isProviderUseFactory narrow an unknown provider to its binding shape. inject uses them internally; they are exported for advanced composition.
import { isProviderUseValue } from '@remotex-labs/xinject';
isProviderUseValue({ provide: ENV, useValue: 'development' }); // true