Yieldless
Reference

yieldless/di

Reader-lite dependency binding for plain functions.

yieldless/di is intentionally small. It binds stable dependencies at the application edge and returns the executable version of the function.

Exports

  • type Injectable<Deps, Args extends unknown[], Return> = (deps: Deps, ...args: Args) => Return
  • inject(core, deps): (...args) => Return

Example

import { inject } from "yieldless/di";

const createHandler = (
  deps: {
    logger: { info(message: string): void };
    audit: { write(message: string): Promise<void> };
  },
  repoId: string,
) => {
  deps.logger.info(`Loading ${repoId}`);
  return deps.audit.write(`repo:${repoId}`);
};

const handler = inject(createHandler, {
  logger: console,
  audit,
});

Why this stays readable

  • All required dependencies are still visible in the function signature
  • There is no hidden container lookup
  • TypeScript enforces that the injected object satisfies Deps before the returned function can be called

Use it for

  • Route handlers configured with repositories, loggers, and feature flags
  • CLI commands configured with a filesystem or process adapter
  • Background jobs configured with queues or telemetry sinks

Good

Put stable dependencies in the first argument and domain inputs after that.

type Deps = {
  readonly logger: { info(message: string): void };
  readonly users: { find(id: string): Promise<User | null> };
};

const loadUser = async (deps: Deps, id: string) => {
  deps.logger.info(`loading ${id}`);
  return await deps.users.find(id);
};

export const handler = inject(loadUser, {
  logger,
  users,
});

Avoid

Do not hide dependencies behind a global lookup.

const loadUser = async (id: string) => {
  const users = container.get("users");
  return await users.find(id);
};

The point of inject() is that the dependency list remains visible and type-checked.

On this page