Skip to content

Guide

Introduction

Build and run an explicit, type-safe dependency graph with @favy/di.

@favy/di turns ordinary TypeScript functions into named, composable dependency providers. There are no decorators or containers: a module declares the dependency object it expects, and the application supplies the graph when it calls its top-level module.

Install

npm install @favy/di

The package works with npm-compatible package managers, including Yarn, pnpm, and Bun. Its declarations require TypeScript 5.0 or newer.

Your first module

Module() creates a named callable. The callback computes the callable’s result.

import { Module } from '@favy/di';

const Ready = Module()('Ready', () => 'Server ready!');

console.log(Ready()); // "Server ready!"
console.log(Ready.name); // "Ready"

The callable’s name is the exact key passed to the constructor. A module with no application dependencies can be called with no arguments.

Declare dependencies explicitly

Pass the dependency object type to Module<Deps>(). TypeScript checks both the callback and every root call against that declaration.

import { Module } from '@favy/di';

interface AddOneDeps {
  value: number;
}

const AddOne = Module<AddOneDeps>()('AddOne', ({ value }) => value + 1);

console.log(AddOne({ value: 5 })); // 6

The library does not infer dependencies by inspecting a function body. You declare AddOneDeps; TypeScript then infers the callback result and reports missing or incorrectly typed inputs.

Compose modules with Live

Live<typeof SomeModule> contains two parts:

  • every dependency required to run SomeModule, including requirements inherited from modules below it;
  • the result of SomeModule, under its declared name.

That makes dependencies transitive and keeps the complete graph visible at the composition root.

import { Module, type Live } from '@favy/di';

const Config = Module<{ environment: 'dev' | 'prod' }>()(
  'Config',
  ({ environment }) => ({ apiUrl: 'https://' + environment + '.example.com' }),
);
type ConfigLive = Live<typeof Config>;

const Service = Module<ConfigLive>()('Service', ({ Config }) => ({
  endpoint: Config.apiUrl + '/users',
}));
type ServiceLive = Live<typeof Service>;

const App = Module<ServiceLive>()('App', ({ Service }) => Service.endpoint);

const result = App({
  environment: 'dev',
  Config,
  Service,
});

console.log(result); // "https://dev.example.com/users"

Here ConfigLive is structurally equivalent to { environment: 'dev' | 'prod'; Config: { apiUrl: string } }. ServiceLive carries those fields forward and adds the keyed Service output. Consequently, App’s root call supplies environment, Config, and Service.

The composition root

The top-level call is the composition root: the one place where concrete values and provider callables are assembled. A dependency entry may be its final value or a module that produces that value. The complete dependency map may be any non-null object, including a class instance or an object with a custom prototype. Its own string and symbol keys participate in resolution; inherited properties do not.

The default Module factory is lazy. At a root call, it installs the supplied providers into a shared dependency context and evaluates a provider only when a module reads that key. Its default cache: 'run' then reuses the resolved value for the rest of that root call. Calling the root module again starts a new run with a new run cache.

This also makes replacement straightforward. The static dependency contract does not shrink just because a nested output is replaced: Live<typeof Service> still includes Service’s transitive Config requirements.

import { Module, type Live } from '@favy/di';

const Config = Module<{ environment: string }>()(
  'Config',
  ({ environment }) => ({ label: environment }),
);
type ConfigLive = Live<typeof Config>;

const Service = Module<ConfigLive>()('Service', ({ Config }) => ({
  describe: () => 'service:' + Config.label,
}));
type ServiceLive = Live<typeof Service>;

const App = Module<ServiceLive>()('App', ({ Service }) => Service.describe());

const fakeService = {
  describe: () => 'service:test',
};

const result = App({
  environment: 'test',
  Config,
  Service: fakeService,
});

console.log(result); // "service:test"

Because App reads the supplied Service value directly, lazy resolution does not execute Config in this run. The environment and Config keys are nevertheless required by the declared ServiceLive type. Use .provide() when you want to bind some or all of those explicit requirements ahead of the root call.

Next steps