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.
Providers
Once you have defined Tokens to identify your dependencies, you need to tell NexusDI how to actually create the values associated with those tokens. This is the role of a Provider.
A provider is essentially a recipe. When the container encounters a token it needs to resolve, it looks up the assigned provider to determine if it should instantiate a class, return a static value, or run a factory function.
The Default: Class Providers
The most common way to provide a dependency is by using a class. In NexusDI, a class can act as its own provider. provide(COMPUTER, { useClass: QuantumComputer }) binds the class to the token, and provide()’s other options pick the other forms: useValue, useFactory and useExisting.
Using static deps
To make a class “injectable,” you define its constructor dependencies in a static deps array. This allows NexusDI to know exactly what to inject without needing runtime reflection.
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;
}
class QuantumComputer implements IShipComputer {
static deps = [REACTOR] as const;
constructor(private readonly reactor: IReactorCore) {}
status() {
return `Reactor output ${this.reactor.output} GW.`;
}
}
await using ship = await Nexus.create([
provide(REACTOR, { useClass: FusionReactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
]);
const status = ship.get(COMPUTER).status(); // -> 'Reactor output 1.21 GW.'
console.log(status);By declaring static deps, TypeScript can verify that the tokens you’ve listed actually match the types expected by the constructor. static deps lists the tokens in parameter order, and provide() reads it, so the provider needs no deps option.
If you forget to declare static deps for a class with constructor parameters, Nexus.create will fail with NEXUS_BLUEPRINT_INVALID, whose errors list holds NEXUS_MISSING_DEPS, preventing a TypeError at runtime. The error carries the number of parameters it found. See NEXUS_MISSING_DEPS. This region shows it:
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;
}
class QuantumComputer implements IShipComputer {
constructor(private readonly reactor: IReactorCore) {}
status() {
return `Reactor output ${this.reactor.output} GW.`;
}
}
const error = await Nexus.create([
provide(REACTOR, { useClass: FusionReactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
]).catch((caught: unknown) => caught);
if (!isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')) throw error;
const first = error.errors[0]?.message; // -> '[NEXUS_MISSING_DEPS] token=ShipComputer module=root arity=1 useClass=QuantumComputer. https://nexus.js.org/errors/NEXUS_MISSING_DEPS'
console.log(first);Try it: Save the first program as providers.ts and run npx tsx providers.ts. The terminal prints Reactor output 1.21 GW. Then delete the static deps line and run it again. npx tsc reports nothing, because the class declares no list to check, and the run ends with an uncaught NEXUS_BLUEPRINT_INVALID error whose second line names token=ShipComputer and arity=1. Put the line back and the program prints Reactor output 1.21 GW. again.
Providers for External Classes
If you are using a class from a third-party library, you cannot add static deps to its source code. In this case, you provide the 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 a package: you cannot add `static deps` to it.
class VendorComputer implements IShipComputer {
constructor(private readonly reactor: IReactorCore) {}
status() {
return `Vendor computer. 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(); // -> 'Vendor computer. Reactor output 1.21 GW.'
console.log(status);The deps option has the same type check as static deps, and when a class carries both, the deps option wins.
Try it: Change deps: [REACTOR] to deps: [] and run npx tsc. TypeScript rejects the provide(COMPUTER, ...) call, because VendorComputer takes one parameter and the list holds none. Run the file anyway and status() throws a TypeError, because reactor is undefined. Change the list back to [REACTOR] and npx tsc reports nothing.
Advanced Provider Types
Sometimes a simple class instantiation isn’t enough. NexusDI provides several other ways to resolve tokens.
Factory Providers (useFactory)
Factories are used when the creation logic is complex or requires asynchronous setup. A factory is a function that returns the value for the token. Its deps list the tokens the function takes as arguments, and provide() types each argument from its token, so link below is an ISubspaceLink with no annotation.
import { Nexus, Token, provide } from '@nexusdi/core';
interface ISubspaceLink {
readonly frequency: number;
}
interface INavCharts {
plot(target: string): string;
}
const FREQUENCY = new Token<number>('Frequency');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
await using ship = await Nexus.create([
provide(FREQUENCY, { useValue: 1420 }),
provide(SUBSPACE_LINK, {
useFactory: (frequency) => ({ frequency }),
deps: [FREQUENCY],
}),
provide(NAV_CHARTS, {
useFactory: async (link) => {
await Promise.resolve(); // stands in for a download over the link
return { plot: (target) => `course to ${target} on ${link.frequency}` };
},
deps: [SUBSPACE_LINK],
}),
]);
const course = ship.get(NAV_CHARTS).plot('Kepler-442b'); // -> 'course to Kepler-442b on 1420'
console.log(course);Factories can be async. NexusDI awaits all async factories during the startup phase, meaning that by the time your app is “online,” every dependency is already resolved and get() remains synchronous.
Try it: Run the program and read course to Kepler-442b on 1420. Change the frequency value to 1701 and run it again. The line ends on 1701, because the factory reads the link the container built from that value. Change it back to 1420 and the first line returns.
Aliasing Tokens (useExisting)
You may want multiple tokens to resolve to the same single instance. useExisting creates an alias, pointing one token to another.
import { Nexus, Token, provide } from '@nexusdi/core';
interface IShipComputer {
status(): string;
}
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const NAV_COMPUTER = new Token<IShipComputer>('NavComputer');
class QuantumComputer implements IShipComputer {
status() {
return 'ShipComputer online.';
}
}
await using ship = await Nexus.create([
provide(COMPUTER, { useClass: QuantumComputer }),
provide(NAV_COMPUTER, { useExisting: COMPUTER }),
]);
const shared = ship.get(NAV_COMPUTER) === ship.get(COMPUTER); // -> true
console.log(shared);The alias builds nothing of its own, so both tokens return the same instance.
Try it: Run the program and read true. Replace useExisting: COMPUTER with useClass: QuantumComputer and run it again. shared prints false, because the second provider builds a second computer. Put useExisting: COMPUTER back and it prints true.
Optional Dependencies
Not every dependency is required for a component to function. By wrapping a token in optional(), you tell NexusDI that it’s okay if no provider exists for that token.
import { Nexus, Token, optional, provide } from '@nexusdi/core';
interface ISubspaceLink {
send(message: string): string;
}
interface IDiagnosticsPanel {
report(): string;
}
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const DIAGNOSTICS_PANEL = new Token<IDiagnosticsPanel>('DiagnosticsPanel');
class StatusBoard implements IDiagnosticsPanel {
static deps = [optional(SUBSPACE_LINK)] as const;
constructor(private readonly link: ISubspaceLink | undefined) {}
report() {
return this.link === undefined
? 'all systems nominal, held aboard'
: this.link.send('all systems nominal');
}
}
await using ship = await Nexus.create([
provide(DIAGNOSTICS_PANEL, { useClass: StatusBoard }),
]);
const report = ship.get(DIAGNOSTICS_PANEL).report(); // -> 'all systems nominal, held aboard'
console.log(report);If the provider is missing, the dependency will resolve to undefined and triggers no startup error. A provider that no visible module exports counts as missing. The constructor parameter takes T | undefined, and TypeScript checks the class against it.
Try it: Delete optional( and its closing parenthesis, leaving static deps = [SUBSPACE_LINK] as const. npx tsc flags the optional import on the first line, now unused. Run the program: Nexus.create rejects with NEXUS_BLUEPRINT_INVALID, and the error it carries is NEXUS_MISSING_PROVIDER for SubspaceLink, requested by DiagnosticsPanel. Put optional(SUBSPACE_LINK) back and the program prints all systems nominal, held aboard again.
Handling Constructor Defaults
If your class has no static deps, NexusDI counts its constructor parameters with cls.length. That count stops at the first parameter with a default or a rest parameter. A class whose parameters all have defaults counts zero and builds with its defaults. A rest parameter follows the same rule as a default: constructor(...rest: X[]) counts zero and builds with an empty rest. constructor(a: X, b = 1) still counts one and fails with NEXUS_MISSING_DEPS.
import { Nexus, Token, provide } from '@nexusdi/core';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
readonly reactor: IReactorCore;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const backupCell: IReactorCore = { output: 0.2 };
class QuantumComputer implements IShipComputer {
constructor(readonly reactor: IReactorCore = backupCell) {}
}
await using ship = await Nexus.create([
provide(REACTOR, { useValue: { output: 1.21 } }),
provide(COMPUTER, { useClass: QuantumComputer }),
]);
const output = ship.get(COMPUTER).reactor.output; // -> 0.2
console.log(output);If you want the container to override those defaults and inject a specific dependency, you must explicitly declare it using static deps or the deps option in provide(). @Injectable from @nexusdi/decorators does the same. With static deps = [REACTOR] as const, the container passes the bound reactor:
import { Nexus, Token, provide } from '@nexusdi/core';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
readonly reactor: IReactorCore;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const backupCell: IReactorCore = { output: 0.2 };
class QuantumComputer implements IShipComputer {
static deps = [REACTOR] as const;
constructor(readonly reactor: IReactorCore = backupCell) {}
}
await using ship = await Nexus.create([
provide(REACTOR, { useValue: { output: 1.21 } }),
provide(COMPUTER, { useClass: QuantumComputer }),
]);
const output = ship.get(COMPUTER).reactor.output; // -> 1.21
console.log(output);Try it: Run the first of the two programs and read 0.2. Add the static deps = [REACTOR] as const line and run it again. The output moves to 1.21. Delete the line and it returns to 0.2.