DEV Community

Discussion on: Rethinking Dependency Injection in TypeScript

Collapse
 
mindplay profile image
Rasmus Schultz

My various experiments with type-safe DI get dumped into this gist, which contains a few different attempts:

gist.github.com/mindplay-dk/64e1f0...

My most successful attempt so far is actually a no-container approach, which I've written an article about, and which also transferred nicely to C#.

My di-v2 approach also works quite nicely in terms of type-safety, and would probably appeal more to you, based on what you seem to like.

I am probably more of a minimalist than you when it comes to dependency injection containers.

For example, I don't want the DI container to govern life-cycles - every component is a singleton, and if I need multiple instances of something, I will register a factory of some sort, so the instancing becomes explicit and visible, either by calling the factory in domain code, or in in the service definitions themselves.

For another, I insist of solving these problems exclusively with types - I don't want configuration for the DI container in domain code, in any form, and I don't want any artifacts in emitted domain code; no decorators or static properties for the DI container to consume. I also insist on things like immutability and pure functions, which seem generally to work better when complex type relationships are involved, as the type expressions are themselves, in a sense, "pure functions calculating types".

I'm a firm believer that most of the problems around DI containers could be more simply and safely addressed by types rather than by run-time facilities - and that, therefore, the run-time facility shouldn't need to be complex, at all. Ultimately, what it should do at run-time, is call factory functions at the right time - and that's all. Everything around validation should be doable at compile-time. I would like to have something that works out everything in the IDE while you're working - something very small that anyone can absorb and fully understand in very little time; I think having fewer "moving parts" at run-time is the best way to achieve that.

My problem with all of these approaches is the lack of modularity, which is something I've been attempting to solve lately. By modularity, I mean some way to provide reusable container bootstrapping. In my current toy example, for example, I have a "system provider", which provides things like a root-path and a temp-path - and a "user services provider", which contains a file-cache that requires a temp-path.

So the challenge here is to provide some means of declaring that the "user services provider" depends on the "system provider", meaning these must both be bootstrapped in order to have a valid container: "the combined shape of all services provided must extend the combined shape of all services required".

You can see my so-far failed attempt here.

Apparently, this is not easy to achieve. If you look at lines 36-46, you can see all of my failed attempts to come up with a function signature for the factory creation function. What I'd like, is for the createContainer call to fail (at compile-time) if I omit the systemProvider, which provides the tempPath for the userCache in the userProvider.

If we could get this to work, I feel like we could have something really simple and powerful. I already feel like my other two approaches provide a lot of value and compile-time safety for such a small facility - only, I'm not getting the kind of extensibility and modularity I got with the DI container I wrote in PHP (which, of course, relied on run-time validation and did not provide much in the way of compile-time guarantees or IDE support, and that's one reason I'm really interested in Typescript on the server-side.)

You've clearly spend a lot of time on this as well, so I would be very interested to hear what you think, if you have any input or any different ideas on how to approach this? 🙂

Collapse
 
mindplay profile image
Rasmus Schultz

By the way, here is an earlier version of my latest container attempt, which does work:

gist.github.com/mindplay-dk/f695e6...

However, this approach requires you to manually combine the right types with the right providers - so, while this does achieve the intended validation (if you comment-out the ...systemServices() from the container creation call) there is nothing to guarantee you're actually combining the right things... This is much easier to understand though, and it works, so it might give you some idea of the direction I'm trying to head with that experiment. 🙂