From a DX review of the constraints this repo holds deliberately.
Every provider declares its dependencies twice — once as a tuple, once as a parameter list — and correctness is by position:
TemporalWorkflowActivities(orderContract, "fulfillOrder")(
[PlaceOrder, OrderRepository, StockService, ShippingService],
{ sync: (place, repository, stock, shipping) => … },
)
Reordering the array silently rebinds the parameters. TypeScript catches a swap only when the two service shapes differ, and they routinely do not: two config ports that are both { url: string }, two repositories with the same method set, two value ports over the same primitive. ServiceOf erases the port's brand — sync receives services, not port instances — so the nominal identity that makes the rest of di safe is exactly what is not available at this call.
A keyed form removes the coupling:
({ place: PlaceOrder, stock: StockService }, { sync: ({ place, stock }) => … })
This costs almost no new type machinery. ServicesOf (packages/di/src/provider.ts:6) is already a homomorphic mapped type:
type ServicesOf<D extends readonly AnyPort[]> = { readonly [K in keyof D]: ServiceOf<D[K]> };
That maps over a record unchanged — ServicesOf<{ place: PlaceOrder }> is { place: ServiceOf<PlaceOrder> }. NeedsOf becomes InstanceType<D[keyof D]> instead of InstanceType<D[number]>. Array.isArray already discriminates arms elsewhere in this file and in both worker starters, so a third arm is the established shape.
Why this ranks first among DX changes: it is paid on every provider anyone writes from here on, in every package and every example. Nest gets named injection free from decorators; this gets it free from destructuring, without reflection and without giving up a single compile-time guarantee.
Secondary benefits worth noting: diffs stop being order-sensitive, a dependency can be added without touching unrelated parameters, and the deps list reads as documentation at the call site.
Acceptance
Provider(port) accepts a record of ports alongside the existing tuple, with sync/make/class/acquire receiving a matching record.
- The positional form stays — this is additive, not a migration.
- The port-minting helpers that wrap
Provider (HttpController, AmqpHandler, TemporalWorkflowActivities, HttpRouter, AmqpHandlers, TemporalActivities) accept it too, or state why they cannot.
packages/di/src/*.test-d.ts pins that a record whose values are not ports is refused, and that the services record is typed per key.
- A decision on whether
examples/ migrates: they are the teaching surface, so whichever form they use is the one people copy.
From a DX review of the constraints this repo holds deliberately.
Every provider declares its dependencies twice — once as a tuple, once as a parameter list — and correctness is by position:
Reordering the array silently rebinds the parameters. TypeScript catches a swap only when the two service shapes differ, and they routinely do not: two config ports that are both
{ url: string }, two repositories with the same method set, two value ports over the same primitive.ServiceOferases the port's brand —syncreceives services, not port instances — so the nominal identity that makes the rest of di safe is exactly what is not available at this call.A keyed form removes the coupling:
This costs almost no new type machinery.
ServicesOf(packages/di/src/provider.ts:6) is already a homomorphic mapped type:That maps over a record unchanged —
ServicesOf<{ place: PlaceOrder }>is{ place: ServiceOf<PlaceOrder> }.NeedsOfbecomesInstanceType<D[keyof D]>instead ofInstanceType<D[number]>.Array.isArrayalready discriminates arms elsewhere in this file and in both worker starters, so a third arm is the established shape.Why this ranks first among DX changes: it is paid on every provider anyone writes from here on, in every package and every example. Nest gets named injection free from decorators; this gets it free from destructuring, without reflection and without giving up a single compile-time guarantee.
Secondary benefits worth noting: diffs stop being order-sensitive, a dependency can be added without touching unrelated parameters, and the deps list reads as documentation at the call site.
Acceptance
Provider(port)accepts a record of ports alongside the existing tuple, withsync/make/class/acquirereceiving a matching record.Provider(HttpController,AmqpHandler,TemporalWorkflowActivities,HttpRouter,AmqpHandlers,TemporalActivities) accept it too, or state why they cannot.packages/di/src/*.test-d.tspins that a record whose values are not ports is refused, and that the services record is typed per key.examples/migrates: they are the teaching surface, so whichever form they use is the one people copy.