Lightweight dependency injection container for JavaScript/TypeScript
—
Lazy initialization utilities for circular dependency resolution, delayed constructor instantiation, and performance optimization through deferred object creation.
Proxy-based class for lazy loading that defers constructor resolution until the instance is actually used.
/**
* Proxy-based delayed constructor for lazy loading
* Defers constructor resolution until instance creation
*/
class DelayedConstructor<T> {
/**
* Creates proxy instance that delays object creation
* @param createObject - Function to create the actual instance
* @returns Proxy instance of type T
*/
createProxy(createObject: (ctor: constructor<T>) => T): T;
}Usage Examples:
// Manual DelayedConstructor usage (typically internal)
const delayedUserService = new DelayedConstructor<UserService>();
const userServiceProxy = delayedUserService.createProxy((ctor) => {
// This function is called only when the instance is first accessed
console.log("Creating UserService instance");
return new ctor(logger, database);
});
// userServiceProxy acts like UserService but creation is deferred
// until first method call or property accessPrimary utility function for creating delayed constructors that resolve circular dependencies and enable lazy instantiation.
/**
* Creates delayed constructor for lazy loading and circular dependency resolution
* Constructor function is provided via callback, enabling forward references
* @param wrappedConstructor - Function returning the actual constructor
* @returns DelayedConstructor instance for lazy instantiation
*/
function delay<T>(wrappedConstructor: () => constructor<T>): DelayedConstructor<T>;Usage Examples:
// Circular dependency resolution
class UserService {
constructor(
private orderService: OrderService,
private logger: Logger
) {}
getUserOrders(userId: string) {
return this.orderService.getOrdersByUser(userId);
}
}
class OrderService {
constructor(
private userService: UserService, // Circular dependency!
private database: Database
) {}
getOrdersByUser(userId: string) {
const user = this.userService.getUser(userId);
return this.database.getOrders({ userId: user.id });
}
getUser(userId: string) {
// This creates a circular call, but delay() resolves it
return this.userService.getUser(userId);
}
}
// Register with delayed constructor to break circular dependency
container.register("UserService", UserService);
container.register("OrderService", {
useClass: delay(() => OrderService) // Delay resolves circular reference
});
// Forward reference resolution
// Useful when classes are defined in dependency order
container.register("EarlyService", {
useClass: delay(() => LateService) // LateService defined later in file
});
@injectable()
class EarlyService {
constructor(private lateService: LateService) {}
}
// LateService defined after EarlyService
@injectable()
class LateService {
doSomething() {
return "Late service action";
}
}Common patterns and best practices for implementing lazy loading with TSyringe.
Usage Examples:
// Lazy singleton with expensive initialization
const expensiveSingletonDelay = delay(() => {
// This code only runs when the service is first resolved
console.log("Initializing expensive singleton");
return class ExpensiveService {
private data: LargeDataSet;
constructor() {
// Expensive initialization
this.data = this.loadLargeDataSet();
}
private loadLargeDataSet(): LargeDataSet {
// Simulate expensive operation
return new LargeDataSet();
}
processData(input: any) {
return this.data.process(input);
}
};
});
container.register("ExpensiveService", {
useClass: expensiveSingletonDelay
}, { lifecycle: Lifecycle.Singleton });
// Conditional lazy loading
const conditionalServiceDelay = delay(() => {
// Determine service type at resolution time
const env = process.env.NODE_ENV;
if (env === "production") {
return ProductionService;
} else if (env === "test") {
return MockService;
} else {
return DevelopmentService;
}
});
container.register("ConditionalService", {
useClass: conditionalServiceDelay
});
// Plugin system with lazy loading
interface Plugin {
name: string;
execute(): void;
}
// Plugins are loaded only when needed
const pluginDelays = [
delay(() => class EmailPlugin implements Plugin {
name = "email";
execute() { console.log("Sending email"); }
}),
delay(() => class SmsPlugin implements Plugin {
name = "sms";
execute() { console.log("Sending SMS"); }
}),
delay(() => class PushPlugin implements Plugin {
name = "push";
execute() { console.log("Sending push notification"); }
})
];
// Register plugins lazily
pluginDelays.forEach((pluginDelay, index) => {
container.register(`Plugin${index}`, { useClass: pluginDelay });
});
// Lazy dependency injection with optional dependencies
@injectable()
class ServiceWithOptionalDependencies {
constructor(
private required: RequiredService,
@inject("OptionalService") private optional?: OptionalService
) {}
performAction() {
this.required.doRequiredWork();
// Optional service is only resolved if registered
if (this.optional) {
this.optional.doOptionalWork();
}
}
}
// Register optional service with delay for performance
container.register("OptionalService", {
useClass: delay(() => {
// Only load this service if it's actually used
console.log("Loading optional service");
return OptionalService;
})
});Advanced patterns for resolving complex circular dependencies.
Usage Examples:
// Complex circular dependency scenario
interface IUserService {
getUser(id: string): User;
getUserWithOrders(id: string): UserWithOrders;
}
interface IOrderService {
getOrder(id: string): Order;
getOrdersForUser(userId: string): Order[];
}
interface INotificationService {
notifyUser(userId: string, message: string): void;
notifyOrderUpdate(orderId: string): void;
}
@injectable()
class UserService implements IUserService {
constructor(
@inject("IOrderService") private orderService: IOrderService,
@inject("INotificationService") private notificationService: INotificationService
) {}
getUser(id: string): User {
return { id, name: `User ${id}` };
}
getUserWithOrders(id: string): UserWithOrders {
const user = this.getUser(id);
const orders = this.orderService.getOrdersForUser(id);
return { ...user, orders };
}
}
@injectable()
class OrderService implements IOrderService {
constructor(
@inject("IUserService") private userService: IUserService,
@inject("INotificationService") private notificationService: INotificationService
) {}
getOrder(id: string): Order {
return { id, amount: 100 };
}
getOrdersForUser(userId: string): Order[] {
// Validate user exists (circular call)
const user = this.userService.getUser(userId);
return [{ id: "1", amount: 100 }];
}
}
@injectable()
class NotificationService implements INotificationService {
constructor(
@inject("IUserService") private userService: IUserService,
@inject("IOrderService") private orderService: IOrderService
) {}
notifyUser(userId: string, message: string): void {
const user = this.userService.getUser(userId);
console.log(`Notifying ${user.name}: ${message}`);
}
notifyOrderUpdate(orderId: string): void {
const order = this.orderService.getOrder(orderId);
console.log(`Order ${order.id} updated`);
}
}
// Register services with delays to break circular dependencies
container.register("IUserService", UserService);
container.register("IOrderService", {
useClass: delay(() => OrderService)
});
container.register("INotificationService", {
useClass: delay(() => NotificationService)
});
// All services can now be resolved successfully
const userService = container.resolve<IUserService>("IUserService");
const userWithOrders = userService.getUserWithOrders("123");// Delayed constructor class
class DelayedConstructor<T> {
createProxy(createObject: (ctor: constructor<T>) => T): T;
}
// Delay function signature
function delay<T>(wrappedConstructor: () => constructor<T>): DelayedConstructor<T>;
// Constructor type
type constructor<T> = {new (...args: any[]): T};
// Wrapped constructor function type
type WrappedConstructor<T> = () => constructor<T>;
// Proxy creation function type
type ProxyCreationFunction<T> = (ctor: constructor<T>) => T;Install with Tessl CLI
npx tessl i tessl/npm-tsyringe