I don’t think I’ll ever understand the affinity for DI libraries. I write a lot of tested code but it’s always just plain old interfaces and constructor arguments for me.
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.
That's something DI does sure, but Spring for example is a fully managed way of talking about stuff in your app. It exposes universal APIs to talk about all the things, and the ways of making things, and config for things. It lets you see what stuff is made.
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.
I assume the top comment was made in relation to TypeScript, not DI in general. A lot of this content explains how it improves on problems compared to Spring, and statements like 'there was no good way to create configurable apps' or avoiding use of Java reflections, which do not seem relevant to TypeScript/JS.
Agreed. Modern Spring brags about not requiring pages of external XML files like it did in the old days. But plain constructors is still an improvement on both.
Configurable apps (see "axis" and "role" concepts in distage) are prohibitevely hard to setup and maintain, dual test with sound opt-in memoization are, probably, practically impossible.
No, you need much more because you want to switch multiple implementations at once, you want to avoid logical conflicts and you want to avoid specifying flags which are redundant in a particular configuration.
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.
A bit surprised (and delighted) to see this on the front page.
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.
I feel like DI frameworks for JavaScript/TypeScript are always too complex, and rely too heavily on decorators to make up for the lack of RTTI. You'd be surprised how far you can get with using string identifiers for dependencies:
What does "phased" mean here? My googlefu is failing to turn up meaningful results for "phased dependency injection", and the readme is unhelpful (and probably AI-generated: "distage follows distage's architecture", lol.)
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?
Generative progamming. The problem is solved in phases (or stages) - at first a DAG (effectively a script in a simple DSL) is produced, then it's interpreted (and there may be different ways to interpret, e.g. you may produce a graphviz file or compute some complexity metrics or validate or whatever).
Does anyone have resources or ideas to share on the merits of dependency injection in js/ts ? To me it almost always feels clunky and antithetical to the spcriptey nature of JS/TS.
It shouldn't. DI as a set of patterns and approaches can be implemented anywhere. The language and the runtime can help you or be a hindrance, but the principles are generic.
Indeed it is. The mere fact that with Claude I got reasonably good prototype in less than 4 hours while manual port to C# took me a week until working prototype is definitely exciting.
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.