Skip to content

aklinker1/zero-ioc

Repository files navigation

@aklinker1/zero-ioc

JSR NPM Version Docs API Reference License

Zero dependency, type-safe, decorator-free Inversion of Control (IoC) container.

Usage

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(private readonly 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");

Register Order

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.

The 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:

Example type error

Additionally, thanks to this type-safety, TypeScript will also report an error for circular dependencies!

Access All Registered Services

To access an object containing all registered services, you have two options:

  1. container.registrations: Returns a proxy object that resolves services lazily when you access them.
    const { userRepo, userService } = container.registrations;
  2. container.resolveAll(): Immediately resolves all registered services and returns 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();

Parameterization

Sometimes you need to pass additional parameters to a service, like config, that aren't previously registered services.

In this case, use parameterize. Any parameters passed in via the second argument don't need to be registered beforehand!

import { createIocContainer, parameterize } from "@aklinker1/zero-ioc";

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

Service Lifetime

Singleton

createIocContainer uses singletones by default. Once your service has been resolved, it will be cached and the same instance will be returned on subsequent calls.

interface UserRepo {
  // ...
}

function createUserRepo() {
  return {
    // ...
  };
}

const container = createIocContainer().register("userRepo", createUserRepo);

const userRepo1 = container.resolve("userRepo");
const userRepo2 = container.resolve("userRepo");
console.log(userRepo1 === userRepo2); // true

Transient

A "transient" service is a service that is created every time it is resolved. Use the transient helper to inform the container that your service should be created each time:

import { createIocContainer, transient } from "@aklinker1/zero-ioc";

interface UserRepo {
  // ...
}

function createUserRepo(): UserRepo {
  console.log("Creating new UserRepo");

  return {
    // ...
  };
}

const container = createIocContainer().register(
  "userRepoFactory",
  transient(createUserRepo),
);

const userRepo1 = container.resolve("userRepo");
const userRepo2 = container.resolve("userRepo");
console.log(userRepo1 === userRepo2); // false

Note that when using container.resolveAll(), you're resolving all dependencies immediately, and any transient services will be created once.

If you need to create an instance every time, you can either:

  1. Use the container.registrations proxy, which will return a new instance on every access.
  2. Don't use transient. Instead, register a function that returns your factory function or class:
    container.register("userRepoFactory", () => createUserRepo);
    const userRepoFactory = container.resolve("userRepoFactory");
    const userRepo1 = userRepoFactory();
    const userRepo2 = userRepoFactory();
    console.log(userRepo1 === userRepo2); // false

Scoped

"Scoped" services are services that are created once per "scope". Zero IoC does not currently support creating "scopes", and thus does not support scoped services.

See #4.

If someone has a good proposal that keeps the API simple, leave a comment on the issue. I'd be happy to consider adding support.


Inspiration

This library was heavily inspired by Awilix, a powerful IoC container for JavaScript/TypeScript. While @aklinker1/zero-ioc is intentionally simpler and more opinionated (focusing on singletons, explicit dependencies, and type-safety through TypeScript), Awilix's approach to dependency registration and resolution without decorators was a major influence.

Feature Comparison

Feature Zero IoC Awilix InversifyJS TSyringe TypeDI
Decorators
Require reflect-metadata
Class-based Services
Factory Function-based Services
Singleton Lifetimes
Transient Lifetimes
Scoped Lifetimes ❌ see #4
Circular Dependency Detection ✅ via TS 🟡 at runtime 🟡 at runtime
Circular Dependency Support 🟡 not recommended
End-to-end Type-safety
Async Resolution 🟡 ✅ via awilix-manager
Module loader
Dependencies (Subdependencies) 0 1 (18) 3 (6) + 1 1 + 1 0 + 1
Package Size (Install Size) 17 kB 326.6 kB (835.6 kB) 32.7 kB (873.7 kB) + 241.2 kB 148.6 kB (182.5 kB) + 241.2 kB 432.8 kB + 241.2 kB

About

Zero dependency, type-safe IoC container

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors