<< Blog Index

Injecting Custom Constructor Arguments into NestJS Objects
February 6, 2025

Honestly, just don't do it. If there's one thing that ChatGPT is bad at, it's answering obscure NestJS questions. If there is another thing it's bad at, it's telling me no when something should not be possible.

Instead, what it does is yank me around with hope while giving me convincing looking code that doesn't quite work.

So, here's what I wanted to do. I'm creating task classes like these:

@DefineTask("Test Task")
export class TestTask implements TaskClass {
   constructor(
      // (insert other services from the container here)
      private readonly host: TaskHost,
   ) {
   }

   async execute(): Promise<boolean> {
      ...
   }
}

I wanted all of my program's tasks to implement this interface, where the "task host" contains the details of the task at hand in addition to utility functions like audit logging that the task can use while executing.

The thing is, the task host is built by the parent service at the time of instantiation.

Naive me figured that it would be no big deal to pass that along to moduleRef.create somehow when instantiating the task class. I thought it would be easy to mix that with the other constructor arguments that come from the container. The short answer is no, it's not possible. NestJS doesn't provide any way to do it. ChatGPT may tell you otherwise, since it's terrible at saying no, so I wasted a good amount of time trying to make it work with research about "context IDs" and such.

The real solution is to not use custom constructor arguments, and instead receive the data through another interface function, e.g., the execute function in this case.

async execute(host: TaskHost): Promise<boolean>;

But what if?

Curious me wanted to make it work anyway. Not because I really needed to use custom constructor arguments in my tasks, but because I wanted to know how it all works under the hood.

To start, the key here is TypeScript. Javascript by itself doesn't have information about how classes are constructed. In other words, Javascript doesn't have reflection. It's TypeScript magic that introduces some reflection concepts and allows NestJS to know how to instantiate objects.

It starts with two cryptic configuration values in the tsconfig.json file:

{
   "experimentalDecorators": true,
   "emitDecoratorMetadata": true
}

The experimentalDecorators enables decorators in the code, the syntax of @Xyz(...) that you can attach to classes and properties and such. emitDecoratorMetadata causes all decorators to add reflection metadata to their targets, available at runtime.

So for example, when you have:

@MyDecorator()
class MyClass {
   ...
}

TypeScript by itself will see that this is a "decorated" class. For any decorated class, it will save some "design" metadata about it. The most useful one is design:paramtypes which dictates what the construction parameter types are. It's a simple array of types, e.g., [string, number, MyService] etc., corresponding to each constructor argument. It doesn't matter what the decorator actually does, any decorator use will cause this design metadata to be emitted (so long as it's enabled in tsconfig).

If Typescript didn't have the design metadata feature, then you would have to decorate each injection manually, always, as there would otherwise be no way for NestJS to determine what to inject. For example:

constructor(
   @Inject(MyServiceA) private readonly serviceA: MyServiceA,
   @Inject(MyServiceB) private readonly serviceB: MyServiceB,
   @Inject(MyServiceC) private readonly serviceC: MyServiceC,
)

Versus when you have design metadata to assist with determining what to inject:

constructor(
   private readonly serviceA: MyServiceA,
   private readonly serviceB: MyServiceB,
   private readonly serviceC: MyServiceC,
)

Much more convenient, even if we are breaking the SOLID principles by depending on a concrete class.

Okay, so back to the goal at hand, getting custom constructor arguments into the instantiation process. Once you understand the design metadata, the solution becomes clearer.

The idea is to inspect the construction metadata and then copy it to a separate factory class. That way, you can intercept the injected arguments, add your own, and then forward the complete list of arguments to the actual class constructor.

Just one more thing, NestJS will complain that it can't resolve your arbitrary constructor argument.

Revert to the task class example:

constructor(
   private readonly myService: MyService, // Injected from the container
   private readonly host: TaskHost, // Injected manually
)

NestJS will complain that it can't resolve TaskHost since it's not defined as a provider. My solution is to have a custom decorator for those arguments:

constructor(
   private readonly myService: MyService, // Injected from the container
   @Supplied("host") private readonly host: TaskHost, // Injected manually
)

What @Supplied would do is similar to @Inject. It updates the metadata to describe that the parameter would be supplied by data from the user under the key "host". It also calls @Inject using a dummy token SUPPLIED_DEP to resolve the NestJS error.

export const SUPPLIED_DEP = Symbol("supplied-dep");
export const SuppliedDepProvider = {
   provide: SUPPLIED_DEP,
   useValue: undefined,
};

With this as a provider, NestJS will use undefined as the value for any "Supplied" parameter, and then it's up to our factory function to fill in the blank. Alternatively, you could remove the custom arguments from the factory's constructor, but that would involve modifying the undocumented metadata which would hurt forward compatibility, not to mention more complex.

The factory function reads the metadata that describes the supplied parameters and understands which arguments to replace with data from the user, and then forwards the updated argument list to the real constructor.

See my working example of the hybrid creation process.

The danger I see with this approach is that we're touching internal reflection data that is not well documented. For example, the SELF_DECLARED_DEPS_METADATA metadata from NestJS is copied. This is what contains the @Inject decorations. There might be other reflection fields that I'm not aware of that are not being handled properly here, and if anything underneath changes, the code would break. Hence, this is more of a learning exercise than a recommended approach.

<< Blog Index