Providers — API Reference
@tagna/udiot v1.0.0-rc.1
Overview
A provider is a plain object that tells the container what token to register and how to produce the corresponding value. All provider interfaces share a provide field (the token) and an optional scope field. The distinguishing field (useClass, useValue, useFactory, or useExisting) determines the production strategy.
import type {
ClassProvider,
ValueProvider,
FactoryProvider,
ExistingProvider,
AnyProvider
} from '@tagna/udiot';
ServiceScope Enum
enum ServiceScope {
SINGLETON = 'singleton',
TRANSIENT = 'transient',
SCOPED = 'scoped'
}
Applies to ClassProvider and FactoryProvider. ValueProvider and ExistingProvider have no scope — the same value or alias is always returned.
| Value | Meaning |
|---|---|
SINGLETON |
One instance per container. Default for registerClass(). |
TRANSIENT |
New instance on every resolve(). |
SCOPED |
One instance per child container created with createChild(). |
ClassProvider
interface ClassProvider<T = any> {
provide: InjectionToken;
useClass: new (...args: any[]) => T;
scope?: ServiceScope;
}
The container reads @Injectable metadata from useClass to discover constructor dependencies, resolves each one recursively, and calls new useClass(...resolvedDeps).
The class must be decorated with @Injectable(). Attempting to register a plain (undecorated) class as a ClassProvider will throw InvalidProviderError at resolution time.
import { ServiceScope } from '@tagna/udiot';
container.register({
provide: UserService,
useClass: UserService,
scope: ServiceScope.SINGLETON
});
// Shorthand
container.registerClass(UserService, UserService);
ValueProvider
interface ValueProvider<T = any> {
provide: InjectionToken;
useValue: T;
// no scope — always returns the same value
}
Useful for:
- Configuration objects assembled from environment variables.
- Third-party library instances that you create yourself.
- String or numeric constants.
container.register({
provide: 'DB_CONFIG',
useValue: {
host: process.env.DB_HOST ?? 'localhost',
port: Number(process.env.DB_PORT ?? 5432),
database: 'myapp'
}
});
// Shorthand
container.registerValue('DB_CONFIG', { host: 'localhost', port: 5432, database: 'myapp' });
FactoryProvider
interface FactoryProvider<T = any> {
provide: InjectionToken;
useFactory: (...args: any[]) => T;
inject?: InjectionToken[];
scope?: ServiceScope;
}
inject is an ordered array of tokens. The container resolves each token and passes the results as positional arguments to useFactory. The factory can be synchronous.
Use factories when:
- Construction involves logic that cannot live in a class constructor.
- You need to call a builder or third-party factory function.
- Instance creation conditionally differs based on resolved dependencies.
container.register({
provide: ConnectionPool,
useFactory: (config: DbConfig) => {
return new ConnectionPool(config.host, config.port, {
max: 10,
idleTimeoutMillis: 30_000
});
},
inject: ['DB_CONFIG'],
scope: ServiceScope.SINGLETON
});
// Shorthand (inject array is the third parameter)
container.registerFactory(
ConnectionPool,
(config: DbConfig) => new ConnectionPool(config.host, config.port),
['DB_CONFIG']
);
ExistingProvider
interface ExistingProvider<T = any> {
provide: InjectionToken;
useExisting: InjectionToken;
// no scope — delegates to the aliased token's provider
}
Makes one token resolve to whatever another token resolves to. The aliased token must be registered independently.
// Register the concrete implementation
container.registerClass(EmailService, EmailService);
// Alias the interface string token to the concrete class
container.register({
provide: 'IEmailService',
useExisting: EmailService
});
// Both tokens now resolve the same EmailService instance
container.resolve('IEmailService') === container.resolve(EmailService); // true
AnyProvider Union Type
type AnyProvider<T = any> =
| ClassProvider<T>
| ValueProvider<T>
| FactoryProvider<T>
| ExistingProvider<T>;
Used by container.register() and container.registerMany().
Type Guards
The package exports runtime type guards for discriminating provider objects:
import {
isClassProvider,
isValueProvider,
isFactoryProvider,
isExistingProvider,
normalizeProvider
} from '@tagna/udiot';
isClassProvider({ provide: Foo, useClass: Foo }); // true
isValueProvider({ provide: 'X', useValue: 42 }); // true
isFactoryProvider({ provide: Foo, useFactory: () => {} }); // true
isExistingProvider({ provide: 'Alias', useExisting: Foo }); // true
normalizeProvider(value) accepts either a provider object or a bare class constructor and always returns a full AnyProvider. It is used internally by container.register().
When to Use Each Provider Type
| Situation | Provider |
|---|---|
| Registering your own service class | ClassProvider |
| Providing a config object or env var | ValueProvider |
Third-party class that doesn't use @Injectable |
FactoryProvider |
| Construction needs conditional logic or builder pattern | FactoryProvider |
| Exposing a class under an interface string token | ExistingProvider |
| Mocking a service in tests | ValueProvider (inject the mock instance) |
Complete Example: Database Configuration Pattern
This example registers a config object with ValueProvider, the service class with ClassProvider, and a third-party ConnectionPool with FactoryProvider.
import { Container, ServiceScope } from '@tagna/udiot';
import { Injectable } from '@tagna/udiot';
// --- Types ---
interface DbConfig {
host: string;
port: number;
database: string;
poolSize: number;
}
// --- Services ---
@Injectable({ scope: ServiceScope.SINGLETON })
class DatabaseService {
constructor(private pool: ConnectionPool) {}
async query(sql: string): Promise<any[]> {
const client = await this.pool.connect();
try {
return await client.query(sql);
} finally {
client.release();
}
}
}
// --- Registration ---
const container = new Container();
// 1. ValueProvider — config from environment
container.register({
provide: 'DB_CONFIG',
useValue: {
host: process.env.DB_HOST ?? 'localhost',
port: 5432,
database: 'myapp',
poolSize: 10
} satisfies DbConfig
});
// 2. FactoryProvider — third-party pool, depends on config
container.register({
provide: ConnectionPool,
useFactory: (config: DbConfig) =>
new ConnectionPool(`postgres://${config.host}:${config.port}/${config.database}`, {
max: config.poolSize
}),
inject: ['DB_CONFIG'],
scope: ServiceScope.SINGLETON
});
// 3. ClassProvider — own service, depends on pool (resolved automatically)
container.registerClass(DatabaseService, DatabaseService);
// --- Resolution ---
const db = container.resolve<DatabaseService>(DatabaseService);
const rows = await db.query('SELECT 1');