TYPESCRIPT

Lightweight Dependency Injection Container

Simple yet powerful DI container with lifecycle management and scopes

TypeScriptDependency InjectionIoCDesign Patterns

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);
  }
}