Ports
A port is a phantom class: never instantiated, it exists so the type system can tell dependencies apart and the runtime can key services by id. Everything else in the library — providers, modules, contexts — is expressed in terms of ports.
Port(id)<Shape>
class OrderRepository extends Port("OrderRepository")<{
readonly findById: (id: string) => AsyncResult<Order, OrderNotFound>;
}> {}Declares an ordinary port. The subclass-of-a-call pattern is what fixes Shape while producing a concrete class you can pass around and re-use in type positions.
- Identity is nominal, by id. Two ports with identical shapes but different ids are unrelated types; a provider for one never satisfies a dependency on the other. The brand is a module-private symbol, so a port instance type cannot be forged structurally.
- The id is also the runtime key. The built container is a flat map keyed by
portId. Two distinct port classes sharing an id are distinct types but the same key — one would shadow the other, so development builds warn:[di] duplicate port id "X" — one will shadow the other. The check is folded out of production builds byNODE_ENVdefine-replacement. - Declaring a port has no other runtime cost or effect.
Port.many(id)<Member>
class HealthCheck extends Port.many("HealthCheck")<{
readonly name: string;
readonly run: () => AsyncResult<"healthy", HealthCheckFailed>;
}> {}Declares a set port. Member fixes what one contribution looks like; the port's own service — what lands in a Context and what Context.get returns — is readonly Member[].
- Several providers may target it, via
Provider.member; on an ordinary port a second provider is a wiring defect. Context.getreturns every contribution, accumulated across module boundaries. No contributors is not an error — the array is empty.- One id, one kind: the same
portIddeclared ordinary in one place and set in another is a wiring defect.
ServiceOf<P>
Recovers the service shape from a port — the type a provider must construct and Context.get returns. Accepts the class or its instance type:
class GetOrderInteractor {
constructor(orders: ServiceOf<OrderRepository>) {
/* ... */
}
}Use it to type application code against a port without importing any adapter. For a set port, ServiceOf yields the accumulated readonly Member[] — one contribution's shape is the port declaration's own type argument.
Scope (type only)
The phantom requirement a resourceful provider (acquire/release, or an onStop hook) adds to its Needs. No service ever exists for it; its only job is to make "this graph owns un-released resources" visible to the type system, so Module.build can refuse such a graph and Module.scoped can discharge it.
Scope is exported as a type only — useful in Module<X, E, Scope> annotations or Exclude<N, Scope> computations. The class value is withheld: it would enable exactly two things, providing Scope and widening it past the type-level guards, and both are hazards. Attempting to provide it is caught at runtime as a wiring defect regardless.
AnyPort
The structural bound every concrete port class satisfies — portId plus a no-arg constructor. Use it to write helpers generic over ports:
const describe = (port: AnyPort): string => port.portId;PortClass / ManyPortClass
The return types of Port(id) and Port.many(id). Exported so a consumer's declaration emit can name them — class X extends Port("X")<S> {} in a library compiled with declaration: true emits a base-class type the compiler must be able to write. You are not expected to write either by hand.