typectx v0.9: Market-Free API and Duplicate Dependency Guards
Version 0.9 removes the createMarket() + market.add() indirection entirely, replaces it with a flat supplier(name) entry point, and adds compile-time duplicate dependency detection. If you found the market ceremony unnecessary, this release is for you.
Let's dive in.
The Big Change: createMarket() Is Gone
Since v0.1, every typectx application started the same way:
import { createMarket } from "typectx"
const market = createMarket()
const $session = market.add("session").request<Session>()
const $db = market.add("db").product({ factory: () => createDbConnection() })
The market served one purpose: maintaining a Set<string> of registered names to prevent duplicates at runtime. In practice, this was almost never useful—TypeScript's type system already catches most name collisions at compile time, and the market object was just boilerplate you had to pass around or import from a shared module.
v0.9 replaces the entire pattern with a single supplier() function:
import { supplier } from "typectx"
const $session = supplier("session").request<Session>()
const $db = supplier("db").product({ factory: () => createDbConnection() })
No market. No shared state. No ceremony. Each call to supplier(name) returns an object with .request() and .product() methods—the same methods you already knew, just without the middleman.
Why This Is Better
- Zero shared state: No market object to import or pass around
- Better code splitting: Suppliers are fully independent; no implicit coupling through a shared registry
- Less boilerplate: Two lines become one
- Simpler mental model:
supplier("name")is the only entry point you need to learn
New Compile-Time Guard: DuplicateDependencyGuard
Removing the market's runtime name registry could seem like a step backward for safety. v0.9 compensates by adding a compile-time guard that's strictly more powerful: DuplicateDependencyGuard.
This new type-level check catches duplicate supplier names within a single product's suppliers, optionals, and assemblers arrays—something the old market registry never checked.
const $a = supplier("shared").request<string>()
const $b = supplier("shared").request<number>()
// Compile-time error: DuplicateDependencyError
const $service = supplier("service").product({
suppliers: [$a, $b], // Both named "shared" — caught at compile time!
factory: ({ shared }) => shared
})
The error surfaces as a DuplicateDependencyError type with a clear ERROR message, just like the existing CircularDependencyError.
SupplierGraphGuard: Combined Validation
Both guards are now composed into a single SupplierGraphGuard type that runs duplicate detection first, then circular dependency detection:
type SupplierGraphGuard<SUPPLIER> =
DuplicateDependencyGuard<SUPPLIER> extends DuplicateDependencyError ?
DuplicateDependencyError
: CircularDependencyGuard<SUPPLIER>
Every call to .product(), .mock(), and .hire() passes through SupplierGraphGuard. If you have a duplicate name, you'll see the duplicate error. If the graph is circular, you'll see the circular error. If both are fine, you get your supplier back.
Edge Case: Widened Names
If your supplier names are widened to string (e.g., from a dynamic factory), TypeScript can't prove uniqueness at compile time. In those cases, the guard passes silently—it only flags duplicates it can statically prove. hire() retains its overwrite semantics for same-name suppliers, which is how mock replacement works.
Internal Changes: assemblersTeam Removed
v0.8 had two team-building functions on every product supplier: team() and assemblersTeam(). The latter included assemblers in its traversal, which was used internally during _build() for ctx() resolution.
v0.9 removes assemblersTeam entirely. The Ctx() function in build.ts now computes the assembler-inclusive team inline:
// v0.8
const assemblersTeam = supplier.assemblersTeam()
// v0.9
const assemblersTeam = [...supplier.team(), ...supplier.assemblers]
This removes a method from every product supplier's runtime shape and simplifies the ProductSupplier interface.
Internal Changes: request.ts Removed
The standalone request() factory module (src/request.ts) is gone. Request supplier creation is now inlined into the supplier() function in src/index.ts, which calls itself recursively for product construction via main.ts. One less module, one less import.
Migration Guide
Step 1: Replace createMarket() + market.add() with supplier()
// Before (v0.8)
import { createMarket } from "typectx"
const market = createMarket()
const $session = market.add("session").request<Session>()
const $db = market.add("db").product({ factory: () => createDbConnection() })
// After (v0.9)
import { supplier } from "typectx"
const $session = supplier("session").request<Session>()
const $db = supplier("db").product({ factory: () => createDbConnection() })
Step 2: Delete your market module
If you had a market.ts file that exported a shared market instance, you can delete it. Suppliers are now standalone.
Step 3: Fix any new duplicate dependency errors
If TypeScript now reports DuplicateDependencyError on any of your suppliers, you have two suppliers with the same name. Rename one of them.
Step 4: Update assemblersTeam usage (if applicable)
If you were accessing supplier.assemblersTeam() directly (unlikely outside of typectx internals), this method no longer exists.
Why These Changes?
- Simpler API surface: One function (
supplier()) replaces two concepts (createMarket()+market.add()) - Stronger safety: Compile-time duplicate detection is strictly more powerful than the old runtime name registry
- Leaner runtime: Removed
assemblersTeamfrom every product supplier and eliminated therequest.tsmodule - Better defaults: No shared mutable state means better code splitting and testability out of the box
The migration is a straightforward find-and-replace for most codebases.
npm install typectx@latest @typectx/react@latest