Chibi Izumi: Phased Dependency Injection for Typescript
Posted2 months agoActiveabout 2 months ago
github.comTechstory
supportivepositive
Debate
20/100
TypescriptDependency InjectionSoftware Design Patterns
Key topics
Typescript
Dependency Injection
Software Design Patterns
The HN community shares and discusses Chibi Izumi, a TypeScript library for phased dependency injection, with a focus on its design and potential applications.
Snapshot generated from the HN discussion
Discussion Activity
Active discussionFirst comment
5d
Peak period
20
132-144h
Avg / period
5.4
Comment distribution27 data points
Loading chart...
Based on 27 loaded comments
Key moments
- 01Story posted
Oct 31, 2025 at 9:10 PM EDT
2 months ago
Step 01 - 02First comment
Nov 6, 2025 at 7:04 AM EST
5d after posting
Step 02 - 03Peak activity
20 comments in 132-144h
Hottest window of the conversation
Step 03 - 04Latest activity
Nov 8, 2025 at 12:13 PM EST
about 2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45778418Type: storyLast synced: 11/20/2025, 12:26:32 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
Essentially, this is a greatly simplified port of distage (my library implementing phased DI for Scala).
Most of the job was done by Claude, the primary point was to showcase phased DI for Typescript, which has many annoyances and limitations, especially when it comes to reflection.
My contributions here were
(a) the approach itself: first we turn functions and constructors into runtime-inspectable entities called Functoids, then we trace binding dependencies from requested roots, do conflict resolution and build a DAG of operations, then we produce instances by traversing the graph in topological order.
(b) a bit unconventional approach to Typescript reflection, which is manual but comes with compile-time validation.
There are many benefits of phased approach to DI, one of the most important benefits is that you can have "configurable apps" (think use-flags for your applications) which are sound, free of logical conflicts and validated early (in case of Scala we even do it at compile time).
Also this approach is extremely easy to comprehend and reproduce (even Claude can do it with some guidance and interventions; I've done ports to several other languages, some with LLM assistance, some manually). While most DIs (especially single-phased ones) are hard to comprehend, maintain and port to other languages/runtimes, for this approach you need to have just one concept implemented - Functoid. The DAG-forming logic fits in 200-300 lines of code and would look the same in any language.
You can use it with code you can't modify (decorators are just convenience helpers, you can do same through bindings DSL with bit less type safety).
TSyringe depends on reflect-metadata and, if my understanding is correct, forces you to use its decorators.
The comparison table is completely subjective and made with just several glances at the readmes of the mentioned libraries. The point was to showcase phased DI for Typescript.
Other parts of our org use a DI framework and I feel like it causes a new class of dependency ordering bugs or missing dependencies. These just don’t exist when everything is passed in the ctor.
DI addresses/can address/affects more than 10 different aspects of application lifecycle. We've described our reasoning in several talks linked at https://github.com/7mind/izumi?tab=readme-ov-file#docs
> I feel like it causes a new class of dependency ordering bugs or missing dependencies
That's precisely where phased approach shines.
Can you provide specific examples here instead?
What I think is most useful in cases like this is before-and-after code snippets showing how the library adds value.
Not in before-after form though, but there was that "I'll eat my hat" discussion exactly in this form: https://www.reddit.com/r/hascalator/comments/aigfux/comment/...?
I’ve worked in C++, C#, rust, and TypeScript personally so I really can’t comment on Java.
https://izumi.7mind.io/distage/basics.html#activation-axis
I feel like all you need there is a ctor param that takes a greeter and pass in whichever you want.
Those params can be cached as necessary or part of larger config POD objects if they are often passed with other dependencies.
Also you want to be able to make sure that your application will start without actually running it. In Scala implementation we do it at compile time for all the possible paths.
I've found there to be something radically compelling about all the hooks Spring allows into it's runtime. The sub-interfaces of Aware offer all sorts of ways to see what is in your runtime, see things getting constructed, see other parts of the lifetime of things.
Asking the container for a thing is the most well known use case, but there's so so much we can learn about our environment at runtime by having these managed containers. Programming used to hint at "Meta-Object Protocols", more expansive forms of objects, and Spring for example delivered us something like that: a higher level better modeled object (and factory and other pieces) than what the runtimes gave us.
Most of the benefits do not depend on any particular language/runtime/stack.
https://github.com/nkohari/forge
(For context: many years ago, I wrote Ninject, one of the more popular DI frameworks for .NET)
> At this point the project is not battle-tested. Expect dragons, landmines and varying mileage.
Exciting.
The only 2 occurrences of "phase" are comments: `// Plan phase: analyze dependencies, detect errors` and `// Produce phase: create instances`. I'm mostly familiar with DI in C#, and SimpleInjector in particular.
Does "phased" mean "we iterate the dep graph to detect lifecycle/circular dep errors"? Similar to how `.verify()` works in SimpleInjector?
https://docs.simpleinjector.org/en/latest/howto.html#verify-...