Skip to content

Advanced

Transform Input

Customize the dependency value passed into every module callback.

transformInput runs immediately before a module callback. Its runtime signature is (deps, name) => transformedDeps, where deps is an invocation-local view backed by the shared run context and name is the current module’s declared key.

Default behavior: withModuleName

The standard Module factory configures transformInput: withModuleName. It adds exactly { Module: { name } } to the callback input.

That metadata belongs to the current invocation: resolving a child dependency does not replace the outer callback’s Module.name. Module is therefore a reserved input key for the default factory. Omit it from application data, or intersect the callback dependencies with ModuleLive when the metadata is needed.

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

type MessageDeps = {
  message: string;
} & ModuleLive;

const Message = Module<MessageDeps>()(
  'Message',
  ({ message, Module }) => Module.name.toString() + ': ' + message,
);

console.log(Message({ message: 'hello' })); // "Message: hello"

ModuleLive is omitted from the callable’s required argument because the transform supplies it. With Module() and no explicit type argument, ModuleLive is the callback’s default input type.

A custom transform replaces the default

Passing transformInput to makeModule replaces withModuleName; the two transforms are not automatically composed. Returning the original dependency object is useful for observation or validation, but it does not add Module.name. A custom transform’s declared metadata type is honored as written, while withModuleName preserves the current module key as the name literal.

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

const PlainModule = makeModule({
  transformInput: <D extends object>(deps: D, name: PropertyKey): D => {
    console.log('Running ' + name.toString());
    return deps;
  },
});

const Double = PlainModule<{ value: number }>()(
  'Double',
  ({ value }) => value * 2,
);

console.log(Double({ value: 4 })); // 8

To keep the default metadata while adding custom work, call withModuleName explicitly from the replacement.

import { makeModule, withModuleName } from '@favy/di';

const NamedModule = makeModule({
  transformInput: (deps, name) => {
    console.log('Preparing ' + name.toString());
    return withModuleName(deps, name);
  },
});

const CurrentName = NamedModule()('CurrentName', ({ Module }) => Module.name);

console.log(CurrentName()); // "CurrentName"

Fields added by an ordinary preserving transform are available to the module callback and do not need to be repeated at the call site.

Preserving versus replacing the input

An ordinary transform must preserve its input and may intersect additional fields into it. TypeScript enforces this through the generic signature <DX extends D>(deps: DX, name) => DX & PD, so a transform that returns an unrelated object is rejected. An inline transform written as <DX extends object>(deps: DX) => ... works for every later dependency map. A transform limited to a base context can use <DX extends Base> inline or an explicit transformInput<Base, Added> annotation; the caller-supplied portion of every module dependency map must then satisfy Base. A key required by Base remains a caller argument because the transform receives that input before it writes the callback value.

Use an HKT when the transformation intentionally replaces or wraps the callback input instead of preserving it.

import { makeModule, type HKT } from '@favy/di';

type Wrapped<T> = {
  wrapped: T;
};

interface WrappedInputHKT extends HKT {
  readonly type: Wrapped<this['_RESULT']>;
}

const WrappedModule = makeModule({
  transformInput: (deps) =>
    ({
      wrapped: deps,
    } as unknown as WrappedInputHKT),
});

const ReadValue = WrappedModule<{ value: number }>()(
  'ReadValue',
  ({ wrapped }) => wrapped.value,
);

console.log(ReadValue({ value: 42 })); // 42

At runtime the transform wraps the dependency context. At compile time Kind supplies the declared module key to _NAME and the module’s dependency type to both _RESULT and _DEPS; this example reads _RESULT, so wrapped.value is known to be a number. HKT includes an internal nominal marker, so ordinary objects with fields such as _NAME are not accidentally interpreted as HKT declarations.