Dependency Injection without decorators in TypeScript

afl_ext

Adrian

Posted on February 13, 2023

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!

💖 💪 🙅 🚩
afl_ext
Adrian

Posted on February 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related