Skip to content

Rethinking Dependency Injection #15106

@stalniy

Description

@stalniy

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe it

The current issue with DI system in Nest.js is:

  1. It relies on legacy decorators
  2. It relies on emitDecoratorMetadata

These are not just implementation details, they introduce meaningful limitations that may impact NestJS projects in the near future.

The TypeScript team has made it clear that emitting metadata via emitDecoratorMetadata is not aligned with the language's long-term goals (TypeScript non-goals). This metadata emission relies on design-time type information that is tightly coupled with the current TS compiler pipeline and is not replicate in any other build tool (except swc which anyway requires webpack in monorepo).

And that future is getting closer:
Microsoft is actively working on a new Go-based implementation of TypeScript. In their roadmap, they mention that some legacy features will not be supported, though they don’t specify which ones yet. I’ve asked for clarification here, but there's no answer so far.

If emitDecoratorMetadata ends up being one of the dropped features (which seems plausible), that would leave NestJS and any libraries relying on it tied to older, slower build chains, unable to fully leverage modern fast toolchains.

I’m aware of previous issues raised about this:

The current stance seems to be that switching to the new TC39 decorators proposal is blocked due to the lack of parameter decorator support. That’s a fair technical limitation but focusing only on that misses the broader picture:

The long-term viability of relying on emitDecoratorMetadata is becoming increasingly questionable.

It’s also worth noting that Angular, the framework that was originally one of the main drivers of TypeScript’s decorator-based patterns, is now moving away from decorators for dependency injection entirely. This should raise a red flag that the decorator+reflection pattern may not be a solid foundation for the future or at least require the second thought.

Describe the solution you'd like

I suggest to replace decorator based dependency injection with inject function. This is the same concept Angular team used to get rid of decorators in their framework.

There are multiple PROS of using inject:

  1. Remove reliance on TypeScript’s internal metadata system and legacy decorators
  2. Improve compatibility with emerging TypeScript toolchains (like the native Go-based version or esbuild or even nodejs with --strip-types option which in combo with ESM may drastically improve development speed)
  3. Reduce the need for complex reflection logic
  4. Enable easier integration with non-TypeScript environments (e.g., JavaScript-only codebases)
  5. Better DX for class inheritance and for token injection

The code though may look quite similar:

Suggested

class Service {}

class AnotherService {
  constructor(
    private readonly service = inject(Service),
  ) {}
}

Current:

@Injectable()
class Service {}

@Injectable()
class AnotherService {
  constructor(
    private readonly service: Service,
  ) {}
}

Teachability, documentation, adoption, migration strategy

The nice thing about inject function is that it almost reflect the same structure as decorator based approach. Also it may be introduced as alternative/experimental way in parallel to existing decorator based solution, so that projects can migrate iteratively.

Though it will require replacing string based token injection with object based token injection. So, this:

const connectionProvider = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: MyOptionsProvider, optionalProvider?: string) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [...],
}

will become this:

const CONNECTION_TOKEN = new InjectionToken<DatabaseConnection>('CONNECTION')
const connectionProvider = {
  provide: CONNECTION_TOKEN,
  useFactory: (optionsProvider: MyOptionsProvider, optionalProvider?: string) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [...],
}

What is the motivation / use case for changing the behavior?

There 2 motivation factors:

  1. Ecosystem shift towards faster toolchain. Even nodejs cli has --experimental-strip-types which drastically simplifies build toolchain and helps to develop with typescript without long delays
  2. NestJS is a fantastic framework with a vibrant community, and it would be great to see it continue to evolve and future-proof itself.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions