Lightweight Dependency Injection Container
Simple yet powerful DI container with lifecycle management and scopes
Code
interface ServiceDescriptor<T = any> {
token: string | symbol | Function;
factory: (...args: any[]) => T;
lifecycle: 'singleton' | 'transient' | 'scoped';
dependencies?: (string | symbol | Function)[];
}
type ServiceToken<T = any> = string | symbol | (__TOKEN_63__ (...args: any[]) => T);
class DIContainer {
private descriptors = new Map<ServiceToken, ServiceDescriptor>();
private singletons = new Map<ServiceToken, any>();
private scopedInstances = new Map<ServiceToken, any>();
private currentScope: string | null = null;
// Registration methods
register<T>({
token,
factory,
lifecycle = 'transient',
dependencies = []
}: {
token: ServiceToken<T>;
factory: (...args: any[]) => T;
lifecycle?: ServiceDescriptor['lifecycle'];
dependencies?: ServiceDescriptor['dependencies'];
}): this {
this.descriptors.set(token, {
token,
factory,
lifecycle,
dependencies
});
return this;
}
registerClass<T>(
token: ServiceToken<T>,
implementation: __TOKEN_76__ (...args: any[]) => T,
lifecycle: ServiceDescriptor['lifecycle'] = 'transient',
dependencies: ServiceDescriptor['dependencies'] = []
): this {
return this.register({
token,
factory: (...args) => new implementation(...args),
lifecycle,
dependencies
});
}
registerInstance<T>(token: ServiceToken<T>, instance: T): this {
this.descriptors.set(token, {
token,
factory: () => instance,
lifecycle: 'singleton',
dependencies: []
});
this.singletons.set(token, instance);
return this;
}
// Resolution methods
resolve<T>(token: ServiceToken<T>): T {
const descriptor = this.descriptors.get(token);
__TOKEN_88__ (!descriptor) {
throw new Error(`Service not registered: ${token.toString()}`);
}
// Check lifecycle
switch (descriptor.lifecycle) {
case 'singleton':
__TOKEN_91__ (!this.singletons.has(token)) {
this.singletons.set(token, this.createInstance(descriptor));
}
return this.singletons.get(token);
case 'scoped':
__TOKEN_97__ (!this.currentScope) {
throw new Error('Cannot resolve scoped service outside of a scope');
}
__TOKEN_101__ (!this.scopedInstances.has(token)) {
this.scopedInstances.set(token, this.createInstance(descriptor));
}
return this.scopedInstances.get(token);
case 'transient':
return this.createInstance(descriptor);
}
}
private createInstance<T>(descriptor: ServiceDescriptor<T>): T {
const dependencies = descriptor.dependencies?.map(dep => {
// Check if dependency is a class constructor
__TOKEN_111__ (typeof dep === 'function' && dep.prototype) {
return this.resolve(dep);
}
return this.resolve(dep as ServiceToken);
}) || [];
return descriptor.factory(...dependencies);
}
// Scopes
createScope(): { run<T>(callback: () => T): T; dispose(): void } {
const scopeId = Symbol('scope');
const previousScope = this.currentScope;
const previousInstances = new Map(this.scopedInstances);
return {
run: <T>(callback: () => T): T => {
this.currentScope = scopeId.toString();
this.scopedInstances.clear();
try {
return callback();
} finally {
this.currentScope = previousScope;
this.scopedInstances = previousInstances;
}
},
dispose: () => {
this.scopedInstances.clear();
__TOKEN_133__ (this.currentScope === scopeId.toString()) {
this.currentScope = null;
}
}
};
}
// Factory method for resolving with additional dependencies
factory<T>(token: ServiceToken<T>): (...additionalDeps: any[]) => T {
__TOKEN_136__ (...additionalDeps) => {
const descriptor = this.descriptors.get(token);
__TOKEN_139__ (!descriptor) {
throw new Error(`Service not registered: ${token.toString()}`);
}
const regularDeps = descriptor.dependencies?.map(dep => this.resolve(dep)) || [];
return descriptor.factory(...regularDeps, ...additionalDeps);
};
}
// Check if service is registered
has(token: ServiceToken): boolean {
return this.descriptors.has(token);
}
// Dispose resources
dispose(): void {
this.singletons.clear();
this.scopedInstances.clear();
this.descriptors.clear();
this.currentScope = null;
}
}
// Decorator support
function Injectable(lifecycle: ServiceDescriptor['lifecycle'] = 'singleton') {
return function <T extends { __TOKEN_155__ (...args: any[]): any }>(constructor: T) {
const token = constructor;
const dependencies = Reflect.getMetadata('design:paramtypes', constructor) || [];
// Store metadata for container to use
Reflect.defineMetadata('di:lifecycle', lifecycle, constructor);
Reflect.defineMetadata('di:dependencies', dependencies, constructor);
return constructor;
};
}
function Inject(token?: ServiceToken) {
return __TOKEN_161__ (target: any, propertyKey: string | symbol, parameterIndex: number) {
const existing = Reflect.getMetadata('di:inject', target) || [];
existing[parameterIndex] = token || Reflect.getMetadata('design:paramtypes', target)?.[parameterIndex];
Reflect.defineMetadata('di:inject', existing, target);
};
}
// Usage examples
// Service definitions
@Injectable('singleton')
class DatabaseService {
connect() {
console.log('Database connected');
}
}
@Injectable('scoped')
class UserRepository {
constructor(private db: DatabaseService) {}
getUsers() {
return [{ id: 1, name: 'John' }];
}
}
@Injectable('transient')
class EmailService {
sendEmail(to: string, message: string) {
console.log(`Sending email to ${to}: ${message}`);
}
}
class UserService {
constructor(
@Inject() private userRepo: UserRepository,
@Inject() private emailService: EmailService
) {}
notifyUsers() {
const users = this.userRepo.getUsers();
users.forEach(user => {
this.emailService.sendEmail(user.name, 'Notification');
});
}
}
// Setup container
const container = new DIContainer();
// Register services
container.registerClass(DatabaseService, DatabaseService, 'singleton');
container.registerClass(UserRepository, UserRepository, 'scoped');
container.registerClass(EmailService, EmailService, 'transient');
container.registerClass(UserService, UserService, 'singleton');
// Manual registration example
container.register({
token: 'Logger',
factory: () => ({
log: (message: string) => console.log(`[LOG] ${message}`)
}),
lifecycle: 'singleton'
});
// Resolve and use
const userService = container.resolve(UserService);
userService.notifyUsers();
// Using scopes
const scope = container.createScope();
scope.run(() => {
const scopedRepo1 = container.resolve(UserRepository);
const scopedRepo2 = container.resolve(UserRepository);
console.log(scopedRepo1 === scopedRepo2); // true (same scope)
});
// Factory pattern
const userServiceFactory = container.factory(UserService);
const userServiceWithCustomDep = userServiceFactory(customDependency);
// Service provider pattern
interface ServiceProvider {
get<T>(token: ServiceToken<T>): T;
has(token: ServiceToken): boolean;
}
const serviceProvider: ServiceProvider = container;
// Child containers
class ChildContainer extends DIContainer {
constructor(private parent: DIContainer) {
super();
}
resolve<T>(token: ServiceToken<T>): T {
__TOKEN_187__ (super.has(token)) {
return super.resolve(token);
}
return this.parent.resolve(token);
}
has(token: ServiceToken): boolean {
return super.has(token) || this.parent.has(token);
}
}