--- name: simple-di description: "A practical pattern for dependency injection in TypeScript using factory functions, plain objects, and zero frameworks." allowed-tools: - Read - Write - Edit - Grep - Glob --- ## Simple DI: Factory-Based Dependency Injection in TypeScript Implement framework-less dependency injection using factory functions, typed deps objects, and a Proxy-based registry. No decorators, no reflect-metadata, no framework. ## Signals to apply - Test files use jest.mock or vi.mock for multiple dependencies per test - Services import other services directly at the module level - Circular dependencies cause runtime errors or slow builds - Adding a dependency to a service requires updating unrelated test mocks ## Implementation steps 1. For each service, define a deps type listing its dependencies as an object: type UserServiceDeps = { db: Database; logger: Logger; emailService: EmailService } 2. Convert the service to a factory function that takes deps and returns an object of methods: export const createUserService = (deps: UserServiceDeps) => ({ getUser: async (id: string) => deps.db.users.findOne({ id }), createUser: async (data: NewUser) => { ... }, }) 3. Create a factory.ts utility (~25 lines) with a Proxy-based registry: - factory(init) registers a lazy initializer, returns a Proxy that throws if accessed before instantiation - factory.instantiateAll() calls all registered initializers - factory.initializeAll() calls onInit() on services that define it (for post-instantiation setup) - Use Object.assign to merge static methods onto the create function 4. In each service file, wrap creation with factory(): export const userService = factory(() => createUserService({ db, logger, emailService })) 5. At the app entry point, boot with two calls: factory.instantiateAll() factory.initializeAll() 6. In tests, call the factory function directly with minimal fakes. Only implement methods the test exercises: const svc = createUserService({ db: fakeDb as Database, logger: { info: () => {} } as Logger }) 7. For circular dependencies: both files import each other's proxy export. The Proxy resolves the cycle because modules export the proxy at load time, not the real instance. ## Key constraints - Never call dependencies during construction (inside the factory initializer). Use onInit() for setup that needs deps. - Adopt incrementally: convert one painful-to-test service first as proof of concept - The Proxy get handler must bind functions to the instance (value.bind(instance)) to preserve this context