-
Notifications
You must be signed in to change notification settings - Fork 2
Doctor Inject
Resolve a dependency by a typed label, by the class itself, or —
untyped — by its name. inject is Doctor's ONE injection primitive: used as a
field initializer or constructor default parameter it wires an instance
while it constructs; used inside a getter it injects lazily.
inject<T>(target: Vial<T> | Label<T>, scope?: string): T;
inject(token: string, scope?: string): unknown;-
inject(Db)—Dba label fromlabel<BlogDb>('Db')— returns whatDoctor.stockput under it, typedBlogDb. The label carries the type; nothing else is needed. -
inject(Config)— the class — returns the registered instance, honouring its lifecycle, typedConfig. A class is a value that carries its own type. -
inject('Config')returns the same instanceDoctor.dispense(Config)would, keyed by the class name (or a label's name) rather than the object — typedunknown. The escape hatch for dynamic wiring; see String tokens.
class Handler {
// EAGER — field initializer: resolves while `new` runs.
logger = inject(Logger);
// EAGER — constructor default parameter: same timing, and a test
// can pass a double explicitly (new Handler(fakeDb)).
constructor(private db = inject(Db)) {}
// LAZY — memoizing getter: resolves on first access. Use it to
// break a dependency cycle, register a vial after construction,
// or keep the dependency out of JSON.stringify/spread.
private __audit?: Audit;
get audit(): Audit {
return this.__audit ??= inject(Audit, 'jobs');
}
}There is deliberately no @Dose-style member decorator: Bun
miscompiles value-supplying member decorators whenever a file holds
more than one decorated class
(oven-sh/bun#30326),
so Doctor's decorators record registrations and never supply values.
When scope is omitted, inject falls back to the scope of the
Doctor operation currently constructing an instance — the scope
argument of the driving Doctor.dispense / Doctor.resolve call:
class Handler {
db = inject(Db); // SCOPED, no scope named here
}
Doctor.resolve(Handler, 'req-7'); // db resolves under 'req-7'Precedence: explicit argument → ambient operation scope → none.
A lazy getter resolves at first access, which usually happens
outside any operation — no ambient scope exists there, and Doctor
deliberately does not let a lazy resolution borrow whatever unrelated
operation happens to be in flight at that moment. Name the scope
explicitly in lazy getters for SCOPED dependencies (as 'jobs' above
does), and call Doctor.checkup() at startup so missing
registrations still fail at boot.
inject('Name') resolves by name — a prescribed class's name or a
stocked label's — and returns unknown: you assert the type. Two uses earn
it: a token that only exists at runtime (read from configuration), and a
lazy getter that must not value-import the class on the other side of a
cycle. For everything else, inject(Class) or a label is
typed and immune to minification.
The string form delegates to Doctor.dispenseByName(name, scope?), which
looks the class or label up in a name index kept in sync by prescribe /
stock / revoke / reset. Use it directly when you need the
loosely-typed (unknown) form:
const config = Doctor.dispenseByName<Config>('Config');Note: dispenseByName takes the scope you give it — the
ambient-scope fallback lives in inject, not here.
-
UnregisteredVialError— when no vial is registered — or nothing is stocked — for the target at runtime. -
ScopeRequiredError— propagated when the resolved vial isSCOPEDand no scope was given explicitly, by the ambient operation, or at all. -
CircularDependencyError— when two eagerinject()initializers point at each other; break the cycle by making one side a lazy getter.
The string token is the class name, so:
- Names must be unique across registered vials (last registration wins), and a name held by a stocked label cannot also be a class's.
- They must survive minification — a bundler that renames classes
(
Config→a) breaks token resolution. Don't rely on this in a minified build; useinject(Class)or a label there instead — a label's name is an explicit string, untouched by minifiers.
-
@Vial — registers the classes
injectresolves - stock — typed labels for ready-made values and labelled factories
-
containers — what "the ambient container"
injectreads actually is