2021-07-23
2182
#typescript
Gregory Pabian
60076
Jul 23, 2021 ⋅ 7 min read

Top 5 TypeScript dependency injection containers

Gregory Pabian Full-stack software developer who loves building products.

Recent posts:

Handling Dates In JavaScript With Tempo

Handling dates in JavaScript with Tempo

Use the Tempo library to format dates and times in JavaScript while accounting for time zones, daylight saying time, and date internationalization.

Amazing Enyichi Agu
Apr 30, 2024 ⋅ 8 min read
A Guide To Deno.cron

A guide to Deno.cron

This guide explores how to use the cron package in Deno, `Deno.cron`, to handle scheduling tasks with specific commands.

Rosario De Chiara
Apr 29, 2024 ⋅ 5 min read
Comparing Mutative Vs Immer Vs Reducers For Data Handling In React

Comparing React state tools: Mutative vs. Immer vs. reducers

Mutative processes data with better performance than both Immer and native reducers. Let’s compare these data handling options in React.

Rashedul Alam
Apr 26, 2024 ⋅ 7 min read
Radix Ui Adoption Guide Overview Examples And Alternatives

Radix UI adoption guide: Overview, examples, and alternatives

Radix UI is quickly rising in popularity and has become an excellent go-to solution for building modern design systems and websites.

Nefe Emadamerho-Atori
Apr 25, 2024 ⋅ 11 min read
View all posts

5 Replies to "Top 5 TypeScript dependency injection containers"

  1. This post misses the most important part of DI that most DI in java does which is overrides.

    Using Guice as an example.
    production:
    Injector injector = Guice.createInjector(new ProductionModule());
    App appTree = injector.createClass(App.class);

    testing
    Injector injector = Guice.createInjector(Modules.override(new ProductionModule()).with(new TestOverrides());
    App appTreeWithMocks = injector.createClass(App.class)

    which of the above DI frameworks do this? This is a critical piece of the usefulness of DI.

    1. If I understood you correctly, you want to inject a different class at different runtimes (production and test).

      For Next.js, we can check how their did that for their testing, for instance in this test suite:
      https://github.com/nestjs/nest/blob/master/integration/hello-world/e2e/guards.spec.ts

      For others DI libraries, we can use child injectors or child containers:
      * https://github.com/nicojs/typed-inject#-child-injectors
      * https://github.com/nicojs/typed-inject#-child-injectors

      In general, the TS/JS ecosystem is less mature compared to Java’s one, also based on my experience DI is not a vastly used pattern in TS codebases.

  2. All five of these rely on `public static` members or decorators – meaning all of these rely on your designing code for the DI container. DI containers should be able to work in general – and with third party code in particular.

    They should not require a footprint in the code outside of the DI bootstrap code, near the entry point of the program – domain code should not need to concern itself with dependency injection at all, it should be handled centrally, in one location, not all over the place.

    A few other containers have tried to get around the issue by adding reflection to the language – usually, this relies on plugins for the official TS compiler, which almost no one uses in practice (the compilation space is largely dominated by Babel, ESBuild, SWC, or other tools that integrate these) and with the plugin API itself remaining an unofficial/internal feature of the official compiler, so that doesn’t really work for me either.

    Here’s one container I’ve seen that doesn’t rely on decorators or compilation:

    https://brandi.js.org/

    I wrote an article about doing dependency injection without a DI container, which is also an option:

    https://dev.to/mindplay/a-successful-ioc-pattern-with-functions-in-typescript-2nac

    This article talks about functions, but the pattern is applicable to classes or anything else as well – here’s an example with classes:

    https://tinyurl.com/2t93furt

    I’ve also tried to design a semi-declarative DI container – this should look more familiar:

    https://tinyurl.com/2p8e9jn2

    This approach isn’t fully declarative – you do need to define a service map separately from the actual service definitions. You might actually enjoy this kind of separation, or you may find it overly ceremonious – that seems to be an individual thing.

    Both approaches are type-safe, but neither approach is “modular” – meaning you can’t have multiple reusable “providers” or “modules”, and you will have to bootstrap everything in one central location. Again, some will think that’s a good thing, others will find it annoying – it comes down to opinions.

    I would love it if somebody would try to develop this idea further. I’ve attempted a number of things myself, and just getting to this point with type-safety took a lot of hard work, and it’s still not clear to me whether this concept can expand to incorporate modules, e.g. with type-checking happening at the call site where you bootstrap your providers/modules into a container.

    I don’t know whether the language can do it all.

    If it can, I’m afraid I’m not smart enough to beat the type-system. 😅

    1. I will try to address all your points, Rasmus 🙂

      I believe the general approach (based on my experience alone) to DI in JS/TS should be as follows:

      1. If the application doesn’t require direct function calls between different components, don’t use DI. This happens when maintaining separation of concerns is crucial due to application design. This usually means you use message buses in your application to convey information.
      2. If your application has a small number of components and each component requires up to 3-4 dependencies, you can still construct the components’ instances it manually in a reasonable number of lines.
      3. When the number of components starts growing, you need to start using DI to maintain code readability.

      The choice of the DI framework should depend on the specific runtime. For backends, I don’t think anyone minds relying on a footprint from decorators (especially that we chose to use JS/TS there). For frontends, bundle size matters, but bundles can be split and the footprint can be calculated. Also, a frontend application with a lot of components might not work efficiently after all.

      Code using DI frameworks with footprint is generally easier to read compared to ones’ without. Basically, if I use DI, I don’t want to construct instances myself, I want the DI to do it for me.

      Type safety in TS is a fickle thing. TS is just a superset of JS that compiles code to the latter. There is a a number of problems (e.g. the higher-kinded types) that cannot be expressed in TS code without some “tricks”. A vast number of TS libraries uses type tricks under the hood so we have a notion of type safety in userland code.

Leave a Reply