Providers
Providers are the fundamental building blocks of Resolid’s dependency injection system. They define how services are created and managed within the container.
Provider Interface
The Provider interface is defined in the core DI system:
export interface Provider<T = unknown> {
token: Token<T>;
factory: () => T;
scope?: Scope;
}
Components
token: A unique identifier for the dependency. Can be:
- A Symbol:
Symbol('LogService')
- A Class constructor:
LogService
- An object with
name and prototype properties
factory: A function that creates and returns the service instance. The factory is executed within an injection context, allowing you to use inject() to resolve dependencies.
scope: Controls the lifetime of the service instance. Defaults to 'singleton' if not specified. See Scopes for details.
Token Types
Tokens are type-safe identifiers for your dependencies:
export type Token<T = unknown> =
| symbol
| (new (...args: any[]) => T)
| {
prototype: T;
name: string;
};
Using Symbols as Tokens
Symbols provide unique, collision-free identifiers:
import { Container } from '@resolid/di';
const API_URL = Symbol('API_URL');
const container = new Container();
container.add({
token: API_URL,
factory: () => 'https://api.example.com'
});
const url = container.get(API_URL);
Using Classes as Tokens
Using the class itself as a token is the most common pattern:
class DatabaseService {
async connect() {
// Connection logic
}
}
container.add({
token: DatabaseService,
factory: () => new DatabaseService()
});
const db = container.get(DatabaseService);
Factory Functions
Factory functions are executed within an injection context, giving you access to the inject() function for dependency resolution.
Simple Factories
container.add({
token: ConfigService,
factory: () => new ConfigService()
});
Factories with Dependencies
Use inject() inside factory functions to resolve dependencies:
import { inject } from '@resolid/di';
class UserService {
constructor(
private logger: LogService,
private db: DatabaseService
) {}
}
container.add({
token: LogService,
factory: () => new LogService()
});
container.add({
token: DatabaseService,
factory: () => new DatabaseService()
});
container.add({
token: UserService,
factory: () => new UserService(
inject(LogService),
inject(DatabaseService)
)
});
Factory Context
The factory function runs within an InjectionContext that provides access to the container’s resolver. This context is managed automatically:
// From container/index.ts:47
const value = new InjectionContext(this).run(() => provider.factory());
The injection context maintains a stack to detect circular dependencies and provide proper error messages.
Registering Providers
Use the Container.add() method to register providers:
const container = new Container();
container.add({
token: LogService,
factory: () => new LogService(),
scope: 'singleton'
});
Multiple Providers
Register multiple providers by calling add() multiple times:
container.add({
token: LogService,
factory: () => new LogService()
});
container.add({
token: CacheService,
factory: () => new CacheService()
});
Provider Arrays in Extensions
When creating extensions, provide an array of providers:
import { Extension } from '@resolid/core';
export const databaseExtension: Extension = {
name: 'database',
providers: [
{
token: DatabaseService,
factory: () => new DatabaseService()
},
{
token: MigrationService,
factory: () => new MigrationService(inject(DatabaseService))
}
]
};
Token Resolution
When you call container.get(token), the container follows this resolution process:
- Circular dependency check: Ensures the token isn’t already being constructed
- Provider lookup: Finds the registered provider for the token
- Singleton cache check: For singleton scope, returns cached instance if available
- Factory execution: Runs the factory function within an injection context
- Cache storage: For singleton scope, stores the instance for future use
// From container/index.ts:25-56
private _resolve<T>(token: Token<T>, optional: boolean): T | undefined {
this._checkCircularDependency(token);
const provider = this._providers.get(token);
if (provider === undefined) {
if (!optional) {
throw new Error(`No provider found for ${toString(token)}`);
}
return undefined;
}
const singleton = provider.scope !== "transient";
if (singleton && this._singletons.has(token)) {
return this._singletons.get(token) as T;
}
this._constructing.push(token);
try {
const value = new InjectionContext(this).run(() => provider.factory());
if (singleton) {
this._singletons.set(token, value);
}
return value as T;
} finally {
this._constructing.pop();
}
}
Advanced Factory Patterns
Configuration-based Factories
const CONFIG_TOKEN = Symbol('Config');
container.add({
token: CONFIG_TOKEN,
factory: () => ({
apiUrl: process.env.API_URL || 'http://localhost:3000',
timeout: 5000
})
});
container.add({
token: ApiClient,
factory: () => {
const config = inject(CONFIG_TOKEN);
return new ApiClient(config.apiUrl, config.timeout);
}
});
Conditional Factories
container.add({
token: LogService,
factory: () => {
if (process.env.NODE_ENV === 'production') {
return new CloudLogService();
}
return new ConsoleLogService();
}
});
Factory Initialization
container.add({
token: DatabaseService,
factory: () => {
const db = new DatabaseService();
// Synchronous initialization
db.loadConfig();
return db;
}
});
For asynchronous initialization, use the bootstrap function in extensions or call initialization methods after retrieving the service from the container.
Best Practices
- Use classes as tokens when possible for better type inference
- Keep factories simple - delegate complex logic to the service itself
- Prefer constructor injection over property injection for better testability
- Use symbols for configuration values to avoid naming conflicts
- Document your tokens especially when using symbols or abstract classes