DEV Community

Cover image for Dependency Injection without decorators in TypeScript
Adrian
Adrian

Posted on • Edited on

Dependency Injection without decorators in TypeScript

Decorators will get a big revamp in TypeScript 5, and while decorators are cool, what about getting rid of them? And what if we can make the DI much better along the way?

As I worked in other languages as well with more established DI ecosystems, I see that a good DI framework should:

  1. Be transparent - your domain should not import anything related to the DI, and it should not know about the DI at all
  2. Support interfaces without silly decorators with the interface name
  3. Automatically provide the implementation for interfaces that are implemented only once
  4. Should work just fine when compiled down to a production package
  5. Autowires - means it will automatically create everything it can with no or very minimal configuration

Matching those points using TypeScript is not trivial. I will ignore decorators for now and their experimental reflection features, and focus on what we will have on hand with TypeScript 5.

There are a few obstacles along the way:

  • TypeScript provides no reflection support whatsoever
  • Due to lack of reflection, getting classes constructor parameter's types is impossible
  • Runtime will also not preload all your classes so you would need to import them somewhere so that the class declaration runs and the class is defined
  • Interfaces are erased during compilation and have zero meaning in the running code
  • Getting a list of implemented interfaces by a given class is impossible

Rough. But it turns out that overcoming all of those problems and matching all of the key features of a good DI implementation is possible.

The key here is Reflection.

Fine, I will do it myself

How to even approach this? We need to get information about every class in the application and a lot of information from it, including implemented interfaces and constructor parameters, without decorators. Right...

What if we could scan our codebase and read every file, find class declarations in them, parse those and extract necessary data from it? Surely there must be a way.

Enter TypeScript compiler API.

The Compiler API can be used to get a tokenized representation of the source code.
So we can do the following to get what we want:

  • Scan over the source directory for typescript files
  • For each file:
    • Parse it to get the abstract syntax tree - a tree of tokens
    • Recursively find all nodes that are class declarations, then for each found:
    • Read the node to find the class name, extends, implements
    • Find the constructor node and read parameters from it
    • Push found data to some array

After this is completed, we will be left with an array of class reflection data objects. Using that, we can generate a typescript source file with that metadata and store it in the source folder itself.

After that, we have class metadata, ready to use.

What now?

In my implementation, after generating the metadata, the saved result is an array of the following objects:

  fqcn: string; // Fully qualified class name - path and name
  name: string; // Class name
  ctor: Promise<Constructor> | null; // Constructor for that the class - null if not public
  implementsInterfaces: string[]; // Interfaces implemented by the class
  extendsClass: string | null; // Parent of the class - null if not extending
  constructorParameters: ParameterData[]; // names and types of constructor parameters
  constructorVisibility: "public" | "protected" | "private"; // Constructor visibility
Enter fullscreen mode Exit fullscreen mode

We can see that we have everything needed to create a working DI.

Now, given each class in the metadata:

  • We how its name, so we can find it by name when needed
  • We know the interfaces it implements, so we can find it by interface names
  • We have its constructor function, so we can create instances
  • We know what parameters that constructor takes, its names, and its type names

Autowiring will be done recursively, we only need to know the name of the type:

  • Find metadata of the class by the provided name
  • If not found, find metadata of the class implementing the provided name
  • For each constructor parameter in the metadata:
    • Autowire the parameter by type name the same way (recursively)
  • With ready parameters array, construct the class
  • Return the constructed instance

This method will work just as fine with classes and interfaces, so both can be used in parameter names and during resolving

And that's it! We successfully implemented a working DI without decorators.

Of course, there are other details that need to be taken into consideration to make the DI fully functional, but those are easy things now that the difficult part is done.

Happy autowiring!

Top comments (5)

Collapse
 
maurerkrisztian profile image
Krisztián Maurer

Interesting approach, nice! What are the advantages of eliminating decorators? Is it a personal preference, or are there specific benefits? I can see how it could open up more possibilities in terms of interfaces, but I'm curious to hear your thoughts on the matter. By the way, I also wrote a DI some time ago, and it was a valuable learning experience.

Collapse
 
afl_ext profile image
Adrian

Thanks! There were two reasons that pushed me into this direction, first, I know that in other languages there are DIs that are completely transparent, classes don't need to be exported, registered, no decorators, no mapping, the DI all reads it from the code itself and manages to provide everything and work. Especially this was apparent for me when interface types are being taken into consideration, even if decorators were used, thats still not full transparency and can be improved. So this thought was in my head for some time, and then I read the news about TS 5.0 plans to move on from their always-experimental decorators feature to the standarized JS proposal. In that move the parameter decorators for example as I remember are not possible, and stuff is generally more difficult without the experimental, soon legacy flag turned on. And then I started work on this library to see if it will be feasible and soon enough it was working! And it matches those requirements I've set - complete transparency for the code, no registering of services, works with interfaces by automatically finding matching implementations, and can work without decorators to avoid the current state of this feature.

Collapse
 
maurerkrisztian profile image
Krisztián Maurer

That sounds reasonable. I'll take a closer look at your code when I have some free time.

Collapse
 
asant profile image
Avi

Love this! Nice GitHub repository with plenty of examples 😊

Collapse
 
afl_ext profile image
Adrian

Thank you ❤! So far I added interfaces reflection with properties and methods, and added properties and methods reflections to classses as well! It's not needed for the DI, but it can be useful when autodocumenting, for example, DTOs. Glad you liked it, I hope you can share it somewhere 😊