This section declares every foundation type used throughout the spec. The implementer must export all of them from
@codefast/di. The table below is a map; each type is specified in its own subsection.
| Type | What it is | Where you meet it |
|---|---|---|
BindingScope |
The three instance lifetimes | .singleton() / .transient() / .scoped() |
BindingIdentifier |
An opaque id for one committed binding | .id(), unbind(id) |
Constructor |
A concrete new-able class |
.to(Class), .toSelf(), deps arrays |
ActivationHandler / DeactivationHandler |
The two per-instance lifecycle callbacks | .onActivation(), .onDeactivation() |
ResolveOptions |
The hint a single resolve carries (name and/or tags) | resolve(token, options), inject(token, options) |
ResolutionContext |
What a dynamic factory receives | .toDynamic((ctx) => …) |
ConstraintContext |
Where the current resolve sits in the dependency graph | .when((ctx) => …), advanced constraints |
DependencySlot |
What one resolvable dependency declares | @injectable([…]) deps, ParamMetadata |
TokenValue |
Extracts Value from a token or constructor |
Type-level helper |
BindingScope
type BindingScope = "singleton" | "transient" | "scoped";BindingIdentifier
An opaque branded number — it cannot be constructed by hand from outside the library. It is handed out by .id() on a
builder, by ResolutionFrame.bindingId, and by BindingSnapshot.id, and it is an identity to hand back to
unbind(id), never a value to parse or display.
declare const BINDING_ID_BRAND: unique symbol;
type BindingIdentifier = number & { readonly [BINDING_ID_BRAND]: true };Constructor
/**
* Concrete constructor — can be called with `new`.
* An abstract class does not satisfy this type; use Token<Value> for abstract classes.
* `never[]` rest parameters keep a class with a typed constructor assignable under `strictFunctionTypes`.
*/
type Constructor<out Value = unknown> = new (...args: Array<never>) => Value;Abstract classes. TypeScript does not allow
new AbstractClass(), so an abstract class does not satisfyConstructor<Value>. To bind an abstract class as a token, useToken<Value>instead.container.bind(AbstractLogger)withAbstractLoggeras an abstract class is a TypeScript error.
ActivationHandler and DeactivationHandler
An activation handler is the last step before an instance is handed out and cached: it can initialise or wrap the instance. A deactivation handler is the teardown step when an instance leaves its scope.
Normative — activation.
- The handler receives the resolution context and the instance.
- It runs after
@postConstruct()and before the instance is cached into its scope. By then the instance has been fullynew-ed, accessor initializers included.- It must return an instance — either the same one, or a Proxy wrapping it.
- If it returns a
Promise, the resolve must beresolveAsync().
Normative — deactivation.
- The handler receives the instance and runs when the instance is evicted from its scope. Its return value is ignored.
- It is called only for the scopes in the table below.
| Scope / kind | Deactivation runs? | When |
|---|---|---|
singleton |
Yes | Container disposed, or the binding unbound |
toConstantValue |
Yes — treated as a singleton, even if never resolved | dispose() / unbind() |
transient |
No — each instance is an orphan once handed to the caller | — |
scoped |
Yes — each child container deactivates the instance it cached | That child disposed, or the binding unbound on it |
Why a constant deactivates without a resolve. A singleton only exists after the first resolve, so if it is never resolved there is nothing to deactivate. A constant is the opposite — the value is supplied by the caller at bind time, so it exists from that moment. If the constant was resolved through
onActivation, the hook receives the value after activation, not the original.
Exact shape:
src/core/types.ts—ActivationHandler,DeactivationHandler.
ResolveOptions
Mental model. A single resolve may carry a hint made of criteria. A criterion is one [key, value] pair minted
from a declared tag key. A name is also a criterion — one of the reserved key slotName — so name and tags feed
one selection model, not two.
Fields. All three are optional:
| Field | Meaning | Relationship to the others |
|---|---|---|
name |
Selects a binding declared with whenNamed(name); typed as the names the token declares (Token API) |
Sugar for the criterion slotName.of(name) |
tag |
Exactly one criterion | Equivalent to a single element of tags |
tags |
An array of criteria, read as a superset filter: it matches a binding whose every declared tag is in this array — not "the binding must carry all of these tags" | Several criteria require tags |
The full matching rule is in Slots and last-wins.
InjectOptions accepts both tag and tags and folds tag into tags, so an InjectionDescriptor only ever carries
one spelling.
Exact shape:
src/core/types.ts—ResolveOptions.
Tag keys and criteria
Normative — a criterion is minted by
TagKey.of(), and only by it.
- A tag key is declared with
tag<Value>(name).key.of(value)returns an internedBindingTag: the same value always yields the same object. (Interning means keeping one canonical object per distinct value and returning it on every request.)BindingTagis branded, so it cannot be constructed by hand.key.peek(value)reads the intern cache without minting. The engine folds a request'snamethrough it, so a name no binding ever declared is never retained.
const Region = tag<"eu" | "us">("app:region");
container.bind(Storage).whenTagged(Region.of("eu")).to(S3);
container.resolve(Storage, { tag: Region.of("eu") });Tag value comparison
Normative — tag values compare by
Object.is, on the fast path too. The intern cache must keep-0separate from+0to preserve this rule.NaNfolds to one criterion, becauseObject.is(NaN, NaN)istrue.
Because interning gives each value exactly one criterion, comparing criteria by identity answers exactly as
Object.is on the value does — which is why a criterion may never be constructed any other way.
Passing tag and tags together
Normative. The request carries the union of both sources — equivalent to
tags: [tag, ...tags], andInjectOptionsfolds it into exactly that shape.
ResolutionContext
ctx is what a dynamic factory (toDynamic / toDynamicAsync) receives. It is not a full container — it opens up
exactly the ability to resolve within the current context.
- Six methods, each taking a token plus the same optional hint:
resolve,resolveAsync,resolveOptional,resolveOptionalAsync,resolveAll,resolveAllAsync. resolveAllthrowsAsyncResolutionErrorif any matching binding is async, and returns[]when nothing matches. Both collection reads return aReadonlyArraythat belongs to the caller: every read hands out a fresh array, the memoized root-level read included, so writing into one can never change what a later read returns.graphholds theConstraintContext— the dependency-graph context used inside awhen()predicate. An ordinary resolve never needs it. Read from a factory, it describes the factory's own level: itsresolutionStackends with the frame of the binding the factory is building, soparentis that binding — what a dependency the factory resolves sees as its parent.
Exact shape:
src/core/types.ts—ResolutionContext.
ConstraintContext
Mental model. ConstraintContext answers "where am I in the current resolve?" — which token is being built, which
binding asked for it, and every binding above that. A when() predicate reads it to decide whether a candidate applies.
Five fields:
| Field | Contents |
|---|---|
resolutionPath |
The token names along the current resolve path, readonly — a chain of labels |
resolutionStack |
The full ResolutionFrames along the construction chain — enough metadata to detect a captive dependency |
parent |
The frame directly above, undefined at the root |
ancestors |
Every frame above parent, root first |
currentResolveOptions |
The hint passed into the current resolve, undefined if there is none |
A ResolutionFrame holds: tokenName (for display in error messages), scope, bindingId, kind, and the
slot of the binding matched for that frame. A slot is the binding's criterion set: tags (every criterion, the
reserved name criterion included) plus name, the derived view of the reserved criterion (undefined if the binding
declares no whenNamed()), and keyMask, a bitmask of the tag keys present that lets a constraint rule out a frame
without walking its tags — see Slots and last-wins.
Normative. A frame's
slotreflects the constraint registered at bind time, not the hint passed at resolve time. The advanced constraints in Advanced Constraints read exactly this field.
BindingKind is one of seven values: class, dynamic, dynamic-async, resolved, resolved-async, constant,
alias.
Exact shape:
src/core/types.ts—ConstraintContext,ResolutionFrame,BindingKind.
resolutionStack ordering and its views
Normative.
resolutionStackis a readonly view of the entire resolution path above the current token — it does not include the token being resolved. It is valid while the predicate runs: the engine reuses the path it views, so a context kept past its call is not guaranteed to stay consistent with itself.- Order: from the root (index 0) to the direct parent (last index).
parentandancestorsare computed views over the same data, and the implementer must keep them consistent:ctx.parent === ctx.resolutionStack.at(-1); // nearest frame, undefined at the root ctx.ancestors === ctx.resolutionStack.slice(0, -1); // everything but the nearest frame
Example — the resolve chain App → Database → Logger (root App, direct parent Database, currently resolving
Logger):
resolutionStack = [App_frame, Database_frame] // index 0 = root
parent = Database_frame // resolutionStack.at(-1)
ancestors = [App_frame] // resolutionStack.slice(0, -1)When resolving App at the root (nothing injects App):
resolutionStack = []
parent = undefined
ancestors = []
resolutionPathvsresolutionStack.resolutionPathis an array oftokenNamestrings, enough to display in an error message ("App → Database → Logger").resolutionStackholds fullResolutionFrames (scope, bindingId, slot) — used by advanced constraints and by validate. Both describe the same path, and a rule stated over one holds over the other.
DependencySlot
The shape one resolvable dependency declares, whichever source it came from: a constructor parameter read through
the MetadataReader, or an element of a toResolved deps array.
interface DependencySlot {
readonly token: Token<unknown> | Constructor;
readonly optional: boolean;
readonly multi: boolean;
readonly name?: string | undefined;
readonly tags?: ReadonlyArray<BindingTag> | undefined;
}Normative — both dependency sources are this shape.
InjectionDescriptor(whatinject,optionalandinjectAllreturn) andParamMetadata(what aMetadataReaderreports per constructor parameter) each extend it, adding only what is theirs: a value type parameter and anindexrespectively. Every rule the specification states about a dependency'soptional,multi,nameandtagstherefore holds for both.
A slot's criteria are a request waiting to be made: injectionSlotToResolveOptions(slot) turns the name and tags of
one into the ResolveOptions that asks for them, omitting a key rather than setting it to
undefined, and answering undefined when the slot states no criteria. The binding side has its own converter
(Resolving what a snapshot points at).
Exact shape:
src/injection/dependency-slot.ts—DependencySlot.
TokenValue
A helper type that extracts Value from Token<Value> or Constructor<Value>:
type TokenValue<Type> = Type extends Token<infer Value> ? Value : Type extends Constructor<infer Value> ? Value : never;