Skip to Content
How do I use NexusDI in a project that keeps experimentalDecorators?

This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.

Using NexusDI with experimentalDecorators

NexusDI uses standard (TC39) decorators. If your project must keep experimentalDecorators: true in tsconfig.json because of another library, you cannot use @nexusdi/decorators directly, as they will throw a NEXUS_LEGACY_DECORATORS error.

However, you can still use NexusDI by declaring your dependencies and modules manually. NexusDI never reads emitDecoratorMetadata: it reads no type a compiler emits, so a class lists its deps in static deps or at the binding.

Why the Decorators Throw Under the Flag

With experimentalDecorators set, TypeScript calls a class decorator with the class alone, where a standard decorator receives a context object as well. Each NexusDI decorator checks for that context and throws NEXUS_LEGACY_DECORATORS when the class is defined, which happens when its file loads. The region makes the legacy call by hand:

import { isNexusError } from '@nexusdi/core'; import { Injectable } from '@nexusdi/decorators'; class QuantumComputer {} // Under experimentalDecorators, TypeScript calls a class decorator with the // class alone. This line makes the same call by hand. const legacyCall = Injectable({ deps: [] }) as unknown as ( target: object, ) => void; let message = ''; try { legacyCall(QuantumComputer); } catch (error) { if (!isNexusError(error, 'NEXUS_LEGACY_DECORATORS')) throw error; message = error.message; } const lines = message.split('\n'); // -> ["[NEXUS_LEGACY_DECORATORS] @Injectable was called as a legacy decorator, and NexusDI's decorators are standard (TC39) decorators.", ' Fix: remove experimentalDecorators from tsconfig, or register the class with provide() and defineModule().'] console.log(lines);

Try it: Save the region as legacy.ts and run npx tsx legacy.ts. It prints an array of two lines: the message and its fix line.

In a project that sets the flag, tsc stops first. It reports TS1238 on a decorated class (@Injectable or @Module) and says the runtime invokes the decorator with 1 argument where it expects 2, and TS1240 on an @Inject accessor. Run the file anyway and the same error is thrown when the class is defined.

There are two fixes:

  • Drop the flag: Remove experimentalDecorators from tsconfig.json if no other library needs it, and the decorators compile as written.
  • Drop the decorators: If the flag stays, leave @nexusdi/decorators out and write what each decorator recorded by hand.
DecoratorWithout decorators
@Injectable({ deps })static deps on the class, or deps on the binding
@Injectable({ lifetime })lifetime on the binding: provide(TOKEN, { useClass, lifetime: 'transient' })
@Module({ providers, exports })defineModule({ name, providers, exports })
@Inject(TOKEN) on an accessora constructor parameter listed in deps, or a useFactory provider with deps

A class may carry another library’s legacy decorators and static deps together: NexusDI reads only static deps. The flag belongs to one tsconfig.json, so classes that use the NexusDI decorators can live in a package whose tsconfig.json leaves it out.

Using static deps and provide()

The core of NexusDI does not use decorators. By using static deps on your classes and provide() in your module definitions, you can bypass the decorator system entirely.

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class QuantumComputer implements IShipComputer { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} status() { return `ShipComputer online. Reactor output ${this.reactor.output} GW.`; } } const Engineering = defineModule({ name: 'Engineering', providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); await using ship = await Nexus.create(Engineering); const status = ship.get(COMPUTER).status(); // -> 'ShipComputer online. Reactor output 1.21 GW.' console.log(status);

This approach works regardless of your compiler flags. TypeScript still verifies that the tokens in static deps match the constructor parameters, and the container resolves them normally. TypeScript checks this at the provide() call.

Try it: Save the region as wire.ts and run npx tsx wire.ts. It prints ShipComputer online. Reactor output 1.21 GW. Add "experimentalDecorators": true to the compilerOptions of your tsconfig.json and run it again: npx tsc reports nothing and the output does not change. Now change static deps = [REACTOR] as const to [COMPUTER] as const. npx tsc reports TS2769 on the provide(COMPUTER, { useClass: QuantumComputer }) line, because COMPUTER is not a Token<IReactorCore>. Change it back.

Handling External Classes

If you are using a class from a third-party package that you cannot edit, you can declare its dependencies explicitly within the provide() call:

import { Nexus, Token, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } // A class from another package: it carries no static deps, and you cannot edit it. class VendorComputer implements IShipComputer { constructor(private readonly reactor: IReactorCore) {} status() { return `VendorComputer online. Reactor output ${this.reactor.output} GW.`; } } await using ship = await Nexus.create([ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: VendorComputer, deps: [REACTOR] }), ]); const status = ship.get(COMPUTER).status(); // -> 'VendorComputer online. Reactor output 1.21 GW.' console.log(status);

The deps option takes precedence over any static deps the class might already have, allowing you to wire external classes without needing any decorators at all.

Without the deps option, nothing says what the constructor takes, and Nexus.create rejects with NEXUS_BLUEPRINT_INVALID, whose errors list holds NEXUS_MISSING_DEPS. This region catches the rejection and prints both codes:

import { Nexus, Token, isNexusError, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } // No static deps and no deps option: nothing says what the constructor takes. class VendorComputer implements IShipComputer { constructor(private readonly reactor: IReactorCore) {} status() { return `VendorComputer online. Reactor output ${this.reactor.output} GW.`; } } const error = await Nexus.create([ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: VendorComputer }), ]).catch((caught: unknown) => caught); const code = isNexusError(error) ? error.code : ''; // -> 'NEXUS_BLUEPRINT_INVALID' const errors = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID') ? error.errors : []; const inner = errors.map((item) => item.code); // -> ['NEXUS_MISSING_DEPS'] console.log(code, inner);

Try it: Save the class-from-package region as vendor.ts and run npx tsx vendor.ts. It prints VendorComputer online. Reactor output 1.21 GW. Delete deps: [REACTOR] from the binding and run it again: Nexus.create rejects with the error above. Put deps: [REACTOR] back and the status line returns.