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:

master state management hydration Nuxt usestate

Nuxt state management and hydration with useState

useState can effectively replace ref in many scenarios and prevent Nuxt hydration mismatches that can lead to unexpected behavior and errors.

Yan Sun
Jan 20, 2025 ⋅ 8 min read
React Native List Components: FlashList, FlatList, And More

React Native list components: FlashList, FlatList, and more

Explore the evolution of list components in React Native, from `ScrollView`, `FlatList`, `SectionList`, to the recent `FlashList`.

Chimezie Innocent
Jan 16, 2025 ⋅ 4 min read
Building An AI Agent For Your Frontend Project

Building an AI agent for your frontend project

Explore the benefits of building your own AI agent from scratch using Langbase, BaseUI, and Open AI, in a demo Next.js project.

Ivaylo Gerchev
Jan 15, 2025 ⋅ 12 min read
building UI sixty seconds shadcn framer ai

Building a UI in 60 seconds with Shadcn and Framer AI

Demand for faster UI development is skyrocketing. Explore how to use Shadcn and Framer AI to quickly create UI components.

Peter Aideloje
Jan 14, 2025 ⋅ 6 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