Skip to content

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.

ts
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:

ts
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 useFactory of a provider;
  • In the factory function specified for an InjectionToken;
  • 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:

ts
{
  provide: LOGGER,
  useFactory: async () => {
    // ...
    return MyLogger(inject(OTHER_DEP));
  },
  async: true
}

Please use:

ts
{
  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.

ts
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:

ts
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:

ts
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.

Released under the MIT License