How a Second Middleware Silently Broke Inngest's TypeScript Types
A deep dive into why stacking two Inngest middlewares silently collapsed step.run's TypeScript type to {} — and the actual root-cause fix.
Adding a second middleware to an Inngest client caused step.run's return type to silently collapse to {}, while a single middleware worked fine. The root cause: each middleware carries a default Jsonify output transform, and these compose into Jsonify<Jsonify<T>> — harmless at runtime, but disastrous at the type level.
The real culprit was an optional property whose mapped-type key filter let undefined leak into the object's key union. TypeScript checks the Pick<T, K> constraint at the generic's definition site, where T is still abstract — so the check passes. It never re-checks that constraint once T is instantiated with a concrete type, producing a type that looks healthy everywhere except when its keys are directly enumerated, where it silently evaluates to the compiler's internal error type and collapses to {} with zero diagnostics.
Two prior community fixes patched symptoms rather than the source: one only handled all-default middleware stacks, the other broke again on types containing unknown or at other composition sites like step.invoke. Notably, type-fest's own sibling helpers, FilterDefinedKeys and FilterOptionalKeys, already excluded undefined the same way — only the Jsonify-specific filter had missed that step.
The actual fix excludes undefined from the key union at its source, fixing every composition site at once, and has been submitted upstream to type-fest. A secondary finding: IsEqual-based type tests can pass even on the broken code because they compare deferred type aliases without forcing evaluation — only property access reveals the real failure.