Zero dependency, type-safe Inversion of Control (IoC) container. Designed specifically for use with singleton services, as I use in my personal projects.
Define your services. You can use classes or factory functions:
- Class constructors can only accept a single argument, which is an object with the dependencies
- Factory functions can only accept a single argument, which is an object with the dependencies
// database.ts
export function openDatabase(): Database {
// ...
}
// user-repo.ts
export function createUserRepo(deps: { db: Database }): UserRepo {
// ...
}
// user-service.ts
export class UserService {
constructor(deps: { userRepo: UserRepo; db: Database }) {
// ...
}
}Once your services are defined, you can register them on a container:
// main.ts
import { openDatabase } from "./database";
import { createUserRepo } from "./user-repo";
import { UserService } from "./user-service";
import { createIocContainer } from "@aklinker1/zero-ioc";
export const container = createIocContainer()
.register({ db: openDatabase })
.register({ userRepo: createUserRepo })
.register({ userService: UserService });And finally, to get an instance of a service from the container, use resolve:
const userService = container.resolve("userService");You can only call register with a service if you've already registered all of its dependencies. For example, if userRepo depends on db, you must register db in a separate call to register before registering userRepo.
Good news is TypeScript will tell you if you messed this up! If you haven't registered a dependency, you'll get a type error when you try to register the service that depends on it:
Additionally, thanks to this type-safety, TypeScript will also report an error for circular dependencies!
To access an object containing all registered services, you have two options:
container.registrations: This is a proxy object, and services will be resolved lazily when you access them.const { userRepo, userService } = container.registrations;
container.resolveAll(): Immediately resolve all registered services and return them as a plain object, no proxy magic. Useful when passing services to a third-party library that doesn't support proxies.const { userRepo, userService } = container.resolveAll();
Sometimes you need to pass additional parameters to a service, like config, that's not a previously registered service.
In this case, you should use the parameterize function! Any parameters passed in via the second argument don't need to be registered beforehand!
const openDatabase = (deps: {
username: string;
password: string;
}): Database => {
// ...
};
const container = createIocContainer().register({
db: parameterize(openDatabase, {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
}),
});