Injection
In most cases you may want to inject your dependencies inside a class. There are several ways to do this.
Constructor injection
Needle DI strongly recommends constructor injection, since it makes the dependencies of your class explicit, more type-safe, and allows for easier unit testing.
Instead of using container.get(token), you can use the inject(token) function here, so no reference to an actual container is needed.
import { inject, injectable } from "@needle-di/core";
import { FooService } from "./foo.service";
import { BarService } from "./bar.service";
@injectable()
class MyService {
constructor(
private fooService = inject(FooService),
private barService = inject(BarService),
) {
}
// ...
}TIP
If you don't know what a token is: consider it the unique reference for your binding. In this case, its just the class reference, but there are many more tokens possible.
Learn more about tokens.
NOTE
Needle DI uses default parameter values for constructor injection. This maximizes type-safety and removes the need for parameter decorators, which aren't yet standardized in ECMAScript.
Although experimental parameter decorators allow for static analysis, this design was chosen on purpose to reduce complexity and bundle size.
Initializer injection
Alternatively, you can also initialize your dependencies as (private) fields:
import { inject, injectable } from "@needle-di/core";
import { FooService } from "./foo.service";
import { BarService } from "./bar.service";
@injectable()
class MyService {
private fooService = inject(FooService);
private barService = inject(BarService);
// ...
}Although this is less verbose, this will not allow you to pass in those dependencies when you construct the class manually, e.g. in unit tests.
About the inject() and injectAsync() functions
Note that the inject() and injectAsync() functions are only available in the "injection context":
- During construction of a class being instantiated by the DI container;
- In the initializer for fields of such classes;
- In a synchronous factory function specified for
useFactoryof a provider; - In the
factoryfunction specified for anInjectionToken; - In a function you run yourself using
container.runInInjectionContext().
If you try to use this function outside this context, it will throw an error. This is because Needle DI needs a reference to a DI container when constructing services globally.
WARNING
When using an asynchronous factory provider, you cannot use the inject() / injectAsync() functions. Please use the provider container instance instead.
So instead of:
{
provide: LOGGER,
useFactory: async () => {
// ...
return MyLogger(inject(OTHER_DEP));
},
async: true
}Please use:
{
provide: LOGGER,
useFactory: async (container) => {
// ...
return MyLogger(container.get(OTHER_DEP));
},
async: true
}Manual Injection
Situations where classes aren't used but rather functions can still benefit from dependency injection.
In these cases, manually passing the singleton container instance as an argument to the function and using the .get() function within provides the same practical functionality as using classes with decorators, just with reduced ergonomics.
import type { Container } from "@needle-di/core";
import { FooService } from "./foo.service";
import { BarService } from "./bar.service";
const createMyService = (container: Container) => {
const fooService = container.get(FooService);
const barService = container.get(BarService);
// ...
}Since the dependencies are explicit and no hidden state is involved, this is still the most predictable option, and it keeps working across await boundaries.
Running in an injection context
If threading the container through every function gets in the way, you can also enter an injection context yourself, using the .runInInjectionContext() method. Everything that runs inside can then use inject() and injectAsync(), no matter how deeply nested, and the return value of your function is passed through:
import { Container, inject } from "@needle-di/core";
import { FooService } from "./foo.service";
import { BarService } from "./bar.service";
const container = new Container();
const createMyService = () => ({
fooService: inject(FooService),
barService: inject(BarService),
});
const myService = container.runInInjectionContext(createMyService);
Your function also receives the container as its first argument, so you can still fall back to .get() where that reads better.
Services are resolved from the container you started the context on, so a child container will resolve its own overrides first and only fall back to its parent afterwards.
WARNING
The injection context is only active while your function runs synchronously.
As soon as it awaits something, the context is restored and inject() will throw again. This is deliberate: keeping the context active across an await would leak it to unrelated code that happens to run while your function is suspended, which would then resolve from the wrong container.
So when passing an async function, make sure that every inject() and injectAsync() call happens before its first await:
import { Container, inject, injectAsync } from "@needle-di/core";
import { FooService } from "./foo.service";
import { BarService } from "./bar.service";
const container = new Container();
await container.runInInjectionContext(async () => {
const fooService = inject(FooService); // ✅ still synchronous
const barService = injectAsync(BarService); // ✅ started synchronously
await barService;
inject(FooService); // ❌ throws: no longer in an injection context
});If you need dependencies after an await, either inject them up front, or use manual injection instead.