typectx v0.6: Simpler API, Deeper Trees
The v0.5 and v0.6 releases bring two major improvements to typectx: a 20x increase in dependency tree depth through tail-recursive type optimizations, and a cleaner, more intuitive API that replaces the cryptic $() and $$() accessors with deps and ctx().
Let's dive in.
v0.5: Breaking TypeScript's Recursion Limits
TypeScript has a built-in recursion limit of ~50 levels for non-tail-recursive types. This might sound like a lot, but in real-world applications with deep dependency trees, you can hit this wall surprisingly fast—especially when using features like assemblers, optionals, and hired suppliers that multiply the recursion depth.
The Problem
In v0.4, the recursive type utilities that power typectx's dependency resolution (TransitiveSuppliers, AllTransitiveSuppliers, FilterSuppliers, etc.) were written in a straightforward, non-tail-recursive style:
// v0.4 - Non-tail-recursive (limited to ~50 depth)
type TransitiveSuppliers<SUPPLIERS extends Supplier[]> =
SUPPLIERS extends [infer FIRST, ...infer REST] ?
FIRST extends ProductSupplier ?
[
FIRST,
...TransitiveSuppliers<FIRST["suppliers"]>,
...TransitiveSuppliers<REST>
]
: // ...
: []
Each recursion level would spawn multiple new branches (FIRST["suppliers"] and REST), consuming the recursion budget exponentially. With a linear chain of just 50 suppliers, you'd exhaust TypeScript's limits.
The Solution: Tail-Recursive Types
v0.5 rewrites all recursive type utilities to be tail-recursive with an accumulator pattern:
// v0.5+ - Tail-recursive (supports ~1000 depth)
type TransitiveSuppliers<
SUPPLIERS extends Supplier[],
ACC extends Supplier[] = [] // Accumulator
> =
SUPPLIERS extends (
[infer FIRST extends ProductSupplier, ...infer REST extends Supplier[]]
) ?
TransitiveSuppliers<
[...FilterSuppliers<FIRST["suppliers"], ACC>, ...REST],
[...ACC, FIRST] // Build result in accumulator
>
: // ...
: ACC // Return accumulator at base case
What This Means for You
- Deep linear chains: You can now have 50+ suppliers in a linear dependency chain without type errors
- Complex hierarchies: More room for wide-and-deep supplier trees
- No code changes required: This is a pure type-level optimization—your runtime code stays the same
v0.6: A Cleaner API
The $() and $$() functions were powerful but cryptic. If you're new to typectx, seeing $($$session) in a factory doesn't exactly scream "access the session dependency."
v0.6 replaces these with clearer names:
| Old (v0.4) | New (v0.6) | Purpose |
|---|---|---|
$ (function) | deps (object) | Access resolved dependency values |
$$($$supplier) | ctx($supplier) | Access contextualized suppliers for assembly |
$($$supplier).unpack() | { name } = deps | Get a dependency's value via destructuring |
Before (v0.4)
const $$sendMoney = market.offer("sendMoney").asProduct({
suppliers: [$$addWalletEntry, $$session],
factory: ($, $$) => {
return (toUserId: string, amount: number) => {
// Access dependency value
const addWalletEntry = $($$addWalletEntry).unpack()
addWalletEntry(-amount)
// Reassemble with new context
const addTargetWalletEntry = $$($$addWalletEntry)
.assemble(index($$session.pack({ userId: toUserId })))
.unpack()
addTargetWalletEntry(amount)
}
}
})
After (v0.6)
const $sendMoney = market.offer("sendMoney").asProduct({
suppliers: [$addWalletEntry, $session],
factory: ({ addWalletEntry }, ctx) => {
return (toUserId: string, amount: number) => {
// Access dependency value - now a simple destructure!
addWalletEntry(-amount)
// Reassemble with new context
const addTargetWalletEntry = ctx($addWalletEntry)
.assemble(index($session.pack({ userId: toUserId })))
.unpack()
addTargetWalletEntry(amount)
}
}
})
The deps Object
The new deps parameter is a simple object with getters. Each property is named after its supplier and returns the resolved value directly. You can destructure it right in the factory signature:
factory: ({ session, expensiveService }, ctx) => {
// session is the actual session value, not a wrapper
console.log(session.userId)
// For lazy suppliers, the factory runs on first property access
// Note: don't destructure lazy deps if you want them to stay lazy!
expensiveService.doWork()
}
This API has been inspired by awilix's Proxy object solution for Javascript dependency injection.
No more $($$supplier).unpack() chains—just clean destructuring.
The ctx() Function
The ctx() function remains a function because its purpose is dynamic: it contextualizes suppliers for reassembly with new resources. The name ctx (short for "context") better communicates this:
factory: ({ session }, ctx) => {
// ctx gives you a contextualized version of the supplier
// that inherits the current dependency graph
const product = ctx($someAssembler)
.assemble(index($newResource.pack(value)))
.unpack()
}
New Naming Convention: $$ → $
In v0.4, we had two naming conventions:
$$session,$$userService— Suppliers (the definitions)$session,$userProduct— Products and resources (the packed/assembled supplies)
The double $$ distinguished suppliers from the single $ products and resources you'd work with after calling $($$supplier).
In v0.6, products and resources are rarely accessed directly—you just destructure deps to get dependency values. Since the single $ prefix is no longer needed for supplies, we can give it to suppliers instead:
| v0.4 (suppliers) | v0.6 (suppliers) |
|---|---|
$$session | $session |
$$userService | $userService |
The $ prefix remains a convention (not required), but it helps distinguish supplier definitions from regular variables at a glance.
Product Structure Changes
The product returned by .assemble() also gets cleaner property names:
const product = $myProduct.assemble(index($config.pack(config)))
// Old (v0.4)
product.$ // The $ function
product.$.keys // Available supply keys
product._.$$ // The $$ function
// New (v0.6)
product.deps // Resolved dependency values
product.supplies // Resolved supply objects (with supplier refs)
product._.ctx // The ctx function
Migration Guide
Step 1: Update Factory Signatures
// Before
factory: ($, $$) => {
/* ... */
}
// After - destructure deps directly!
factory: ({ session, db }, ctx) => {
/* ... */
}
Step 2: Replace $(supplier).unpack() with destructured deps
// Before
factory: ($, $$) => {
const session = $($$session).unpack()
// ...
}
// After
factory: ({ session }, ctx) => {
// session is already available!
// ...
}
Step 3: Replace $$(supplier) with ctx(supplier)
// Before
const product = $$($$assembler).assemble(supplies).unpack()
// After
const product = ctx($assembler).assemble(supplies).unpack()
Step 4: Rename $$ prefixes to $ (optional but recommended)
// Before
const $$session = market.offer("session").asResource<Session>()
const $$userService = market.offer("userService").asProduct({ ... })
// After
const $session = market.offer("session").asResource<Session>()
const $userService = market.offer("userService").asProduct({ ... })
Step 5: Update Product Property Access
// Before
product.$.keys
// After
Object.keys(product.deps)
@typectx/react Updates
The React adapter also adopts the new naming. The main change is that useDeps() replaces useInit$() to match the new terminology.
// Before (v0.4)
factory: (init$, $$) =>
function MyComponent() {
const $ = useInit$(init$)
const data = $($$data).unpack()
// ...
}
// After (v0.6)
factory: (initDeps, ctx) =>
function MyComponent() {
const { data, session } = useDeps(initDeps)
// data and session are ready to use!
// ...
}
Wrapping Up
v0.5 and v0.6 represent typectx maturing from a clever proof-of-concept into a production-ready tool. The type system can now handle enterprise-scale dependency graphs, and the API finally reads like idiomatic TypeScript.
These are breaking changes, but the migration is mechanical—a simple find-and-replace gets you most of the way there.
npm install typectx@latest @typectx/react@latest