Skip to Content
Tokens and interfaces

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.

Tokens and interfaces

A NexusDI token names a dependency the container can provide. A class is its own token, as the classes on the Getting started page were, and a Token<T> names a dependency whose type is an interface, which has no runtime value of its own.

Name an interface with a NexusDI Token<T>

A Token<T> gives an interface a runtime name the container can look up. new Token<INavCharts>('NavCharts') carries the type INavCharts, so get(NAV_CHARTS) returns an INavCharts, and provide(NAV_CHARTS, ...) accepts only something that fits it. The description, 'NavCharts', is the name NexusDI prints in error messages and in graph(). Token requires it.

provide(NAV_CHARTS, { useValue }) binds the token to a value the container hands out as it is. Nexus.create takes a list of provide() entries as well as a list of classes (the short form from the Getting started page):

import { Nexus, Token, provide } from '@nexusdi/core'; interface INavCharts { plot(target: string): string; } const NAV_CHARTS = new Token<INavCharts>('NavCharts'); const starCharts: INavCharts = { plot: (target) => `course to ${target}`, }; await using ship = await Nexus.create([ provide(NAV_CHARTS, { useValue: starCharts }), ]); const course = ship.get(NAV_CHARTS).plot('Kepler-442b'); // -> 'course to Kepler-442b' console.log(course);

Try it: save the program as tokens.ts and run npx tsx tokens.ts. The terminal prints course to Kepler-442b. Then change starCharts so that plot returns a number. npx tsc rejects starCharts, because it no longer fits INavCharts. Change plot back to return a string and npx tsc reports nothing.

NexusDI compares tokens by identity

NexusDI compares two tokens with ===, so a token is the object new Token() returned, and its description takes no part in the lookup. Two tokens with the same description are two different tokens. A program shares a token by importing the one object, usually from a module file that declares it beside its interface.

import { Nexus, Token, isNexusError, provide } from '@nexusdi/core'; interface INavCharts { plot(target: string): string; } const NAV_CHARTS = new Token<INavCharts>('NavCharts'); const BACKUP_CHARTS = new Token<INavCharts>('NavCharts'); await using ship = await Nexus.create([ provide(NAV_CHARTS, { useValue: { plot: (target) => `course to ${target}` }, }), ]); const same = NAV_CHARTS === BACKUP_CHARTS; // -> false console.log(same); const provided = ship.has(BACKUP_CHARTS); // -> false console.log(provided); let code: string | null = null; try { ship.get(BACKUP_CHARTS); } catch (error) { if (!isNexusError(error)) throw error; code = error.code; } const missing = code; // -> 'NEXUS_MISSING_PROVIDER' console.log(missing);

has() answers false for BACKUP_CHARTS, and get(BACKUP_CHARTS) throws NEXUS_MISSING_PROVIDER, even though both tokens print as NavCharts.

Try it: in tokens.ts, declare a second new Token<INavCharts>('NavCharts') and ask the container for it with get(). Run the file: the run stops with an uncaught NEXUS_MISSING_PROVIDER error. Read the code the error carries. Then ask for NAV_CHARTS, the token the provide() call used, and the course prints again.

Bind interfaces with NexusDI tokens to swap an implementation

A consumer that depends on an interface token never names the class behind it, so the binding can change without touching the consumer. QuantumComputer declares static deps = [REACTOR] as const and takes an IReactorCore. A real FusionReactor and a test value fit the same token, and QuantumComputer builds over either:

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 QuantumComputer implements IShipComputer { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} status() { return `Reactor output ${this.reactor.output} GW.`; } } class FusionReactor implements IReactorCore { readonly output = 1.21; } const fakeReactor: IReactorCore = { output: 0 }; async function statusWith(reactor: IReactorCore) { await using ship = await Nexus.create([ provide(REACTOR, { useValue: reactor }), provide(COMPUTER, { useClass: QuantumComputer }), ]); return ship.get(COMPUTER).status(); } const real = await statusWith(new FusionReactor()); // -> 'Reactor output 1.21 GW.' console.log(real); const fake = await statusWith(fakeReactor); // -> 'Reactor output 0 GW.' console.log(fake);

The rest of these docs binds every service this way: an interface with a token named for its role, and a class bound to that token. The Modules page swaps a whole module on the same principle, and @nexusdi/testing replaces one binding in a real graph.

Try it: add a SimulatedReactor class with readonly output = 0.75 and call statusWith(new SimulatedReactor()). QuantumComputer prints Reactor output 0.75 GW., and its code stays as it is.