typectx v0.10: Inference Over Annotations
Version 0.10 reverses one of v0.9's main recommendations. In v0.9, I made ProductSupplier easier to write explicitly because I thought manual supplier annotations would be the best way to improve TypeScript performance in large codebases. In practice, that was the wrong optimization target.
The best-performing way to use typectx is now the simplest one: let TypeScript infer your suppliers, and let typectx store the results of recursion internally instead of exposing a recursive public type everywhere. This release also removes $assemblers entirely and replaces that pattern with direct contextual assembly via ctx($supplier).assemble(...).
Let's dive in.
The Big Change: ProductSupplier Is No Longer Meant for Manual Use
v0.9 tried to make this pattern attractive:
import type { ProductSupplier } from "typectx"
const $db: ProductSupplier<...> = supplier("db").product({
factory: () => connectDb()
})
The idea was reasonable: if TypeScript spends time re-inferring large supplier graphs, maybe explicitly annotating suppliers would help the language server.
But after more testing, that approach did not pay off well enough. It made the public type surface heavier, encouraged users to depend on an internal representation, and did not solve the real source of pain as effectively as I hoped.
The real fix was to make the inferred types themselves cheaper.
The Real Fix: Store Recursion Results, Not Recursive Structure
Before v0.10, ProductSupplier was a recursive public type. That meant the shape exposed through inference and declaration emit could balloon as your graph got deeper.
In v0.10, ProductSupplier no longer models the whole recursive graph directly. Instead, it stores the results of that recursion in generic parameters like:
TO_SUPPLY_resolved_deps
So rather than forcing TypeScript to keep walking and serializing a recursive public structure, typectx computes the recursive parts once and stores the resulting maps.
This makes emitted .d.ts files much leaner, which in turn improves TypeScript server performance significantly in real projects.
Just as importantly, it removes the class of recursive type serialization issues that used to show up in the old troubleshooting guide, including:
TS(7056): The inferred type of this node exceeds the maximum length the compiler will serializeType instantiation is excessively deep and possibly infinite
Breaking Change: $assemblers Are Gone
The other major breaking change in v0.10 is that product configs no longer use an assemblers list as part of the public mental model.
In v0.9, nested contextual assembly often looked like this:
const $Feed = supplier("Feed").product({
suppliers: [$db, $session],
assemblers: [$Post],
factory: ({ db }, ctx) => () => {
return db.getPostIds().map((id) => {
const Post = ctx($Post)
.assemble(index($postId.pack(id)))
.unpack()
return <Post key={id} />
})
}
})
In v0.10, you just assemble the supplier you need in context:
const $Feed = supplier("Feed").product({
suppliers: [$db, $session],
factory: ({ db }, ctx) => () => {
return db.getPostIds().map((id) => {
const Post = ctx($Post)
.assemble(index($postId.pack(id)))
.unpack()
return <Post key={id} />
})
}
})
Notice what changed:
- You no longer predeclare contextual targets in
assemblers: [...] ctx($supplier)is now the whole story- If a supplier needs more request data deeper in the call stack, just reassemble it there
This is a simpler API and a clearer mental model. Context propagation is still a flagship feature of typectx, but it no longer needs a separate $assemblers concept.
Migration Guide
Step 1: Remove explicit ProductSupplier annotations you added for performance
// Before
import type { ProductSupplier } from "typectx"
const $service: ProductSupplier<...> = supplier("service").product({
suppliers: [$db, $session],
factory: ({ db, session }) => createService(db, session)
})
// After
const $service = supplier("service").product({
suppliers: [$db, $session],
factory: ({ db, session }) => createService(db, session)
})
If you were only writing the annotation to help TypeScript performance, delete it and let inference do the work.
Step 2: Remove assemblers: [...] from product configs
// Before
const $Feed = supplier("Feed").product({
suppliers: [$db, $session],
assemblers: [$Post],
factory: ({ db }, ctx) => {
/* ... */
}
})
// After
const $Feed = supplier("Feed").product({
suppliers: [$db, $session],
factory: ({ db }, ctx) => {
/* ... */
}
})
Why These Changes?
- Better real-world performance: Leaner declaration output helps TS Server more than encouraging explicit annotations
- Less confusing guidance: The best practice is now the natural one, let inference do the work
- Smaller public type surface:
ProductSupplieris no longer optimized for manual authorship - Simpler context propagation: No need for the
$assemblersconcept
This release is a good example of why library design sometimes needs a switchback. v0.9 tried to optimize the user-facing type. v0.10 optimizes the actual compiler workload instead.
npm install typectx@latest @typectx/react@latest