Skip to content

Core concept

Partial Application

Bind module dependencies incrementally with the chainable provide method.

Every module has a .provide() method. It binds some dependency keys and returns another callable module whose type contains only the remaining keys. Bound values take runtime precedence even if a structurally wider root variable still contains the same keys. A partial may be any non-null object; .provide() consumes its own string and symbol keys and ignores inherited properties.

Bind values

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

const Calculator = Module<{ x: number; y: number }>()(
  'Calculator',
  ({ x, y }) => x + y,
);

const AddFive = Calculator.provide({ x: 5 });

console.log(AddFive({ y: 3 })); // 8
console.log(AddFive({ y: 10 })); // 15
console.log(AddFive.name); // "Calculator"

The returned callable retains the original declared name.

Chain .provide()

Each call removes the newly bound keys, so partial application can be built in stages.

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

const Expression = Module<{ a: number; b: number; c: number }>()(
  'Expression',
  ({ a, b, c }) => a * b + c,
);

const WithA = Expression.provide({ a: 2 });
const WithAB = WithA.provide({ b: 3 });
const ReadyExpression = WithAB.provide({ c: 4 });

console.log(ReadyExpression()); // 10
console.log(ReadyExpression.name); // "Expression"

TypeScript accepts known keys with compatible values and updates the callable signature after every step based on the supplied partial type.

Bind a provider callable

A dependency can be bound to its final value or to a branded provider created by Module or makeModule. Arbitrary functions remain ordinary dependency values; branded callables always participate in DI resolution. Therefore, for a function-valued dependency, a branded provider must return that function rather than merely having a matching callable signature. A module returned from .provide() keeps the provider marker, so it can be nested inside another module. In a discriminated union, a branch-specific provider is accepted after the same partial selects a compatible branch, or after an earlier .provide() call narrows to that branch.

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

const Prefix = Module<{ prefix: string }>()('Prefix', ({ prefix }) => prefix);

const Uppercase = Module<{ Prefix: string }>()('Uppercase', ({ Prefix }) =>
  Prefix.toUpperCase(),
);

const BoundPrefix = Prefix.provide({ prefix: 'ready' });
const ReadyUppercase = Uppercase.provide({ Prefix: BoundPrefix });

console.log(ReadyUppercase()); // "READY"

Each configured callable owns its bound dependencies. With cache: 'run' or cache: 'none', two variants created from the same base module can bind different values without leaking them into each other, while unresolved dependencies can still use the parent run context. With cache: 'module', the factory-wide cache is keyed by module name, so same-name variants intentionally share whichever result was cached first until flushCache().

Live keeps transitive requirements

Providing a nested module’s keyed output does not automatically remove that module’s transitive dependencies from a parent Live type. Those requirements are explicit fields and must be provided at the parent as well.

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

const Database = Module()('Database', () => ({
  findUser: (id: number) => ({ id, name: 'User ' + id }),
}));
type DatabaseLive = Live<typeof Database>;

const UserRepository = Module<DatabaseLive>()(
  'UserRepository',
  ({ Database }) => ({
    findById: (id: number) => Database.findUser(id),
  }),
);
type UserRepositoryLive = Live<typeof UserRepository>;

const UserService = Module<UserRepositoryLive>()(
  'UserService',
  ({ UserRepository }) => ({
    displayName: (id: number) => UserRepository.findById(id).name,
  }),
);

const BoundRepository = UserRepository.provide({ Database });

// Binding only UserRepository still leaves Database in UserRepositoryLive.
const ServiceWithRepository = UserService.provide({
  UserRepository: BoundRepository,
});

const ReadyService = ServiceWithRepository.provide({ Database });

console.log(ReadyService().displayName(1)); // "User 1"

This explicitness is intentional: Live<typeof UserRepository> contains both DatabaseLive and the keyed UserRepository result, so the composition root can always see the full graph contract. Bind all of those fields when you want a zero-argument callable.

Summary

  • .provide({ key: value }) binds produced values.
  • .provide({ key: provider }) binds module/provider callables.
  • .provide() is chainable, and every returned callable retains its declared name.
  • .provide() tracks remaining fields and supports narrowing discriminated-union dependencies as values are bound.
  • Any non-null object can be a partial; only its own keys are bound.