Skip to content

InjectionToken

Classes can be injected by their own identity, but interfaces, primitives, and config objects cannot - they have no runtime value to use as a key. InjectionToken<T> gives such a dependency a stable, unique, typed key.

ts
import { InjectionToken } from '@remotex-labs/xinject';

const API_URL = new InjectionToken<string>('API_URL');

The type parameter T records what the token resolves to, and the description string is used in error messages such as No provider for InjectionToken(API_URL).

Providing and injecting

Bind the token in providers, then request it with @Inject:

ts
import { inject, Inject, Injectable, InjectionToken } from '@remotex-labs/xinject';

const API_URL = new InjectionToken<string>('API_URL');

@Injectable({ providers: [{ provide: API_URL, useValue: 'https://api.example.com' }] })
class ApiClient {
    constructor(@Inject(API_URL) private url: string) {}
}

inject(ApiClient);

Any provider form works with a token - useValue for a constant, useFactory to build it, or useClass to resolve it to a class.

Injecting an interface

An interface exists only at compile time, so bind a token to a concrete implementation and inject the token wherever the interface is needed:

ts
interface Clock {
    now(): number;
}

const CLOCK = new InjectionToken<Clock>('CLOCK');

@Injectable({ providers: [{ provide: CLOCK, useFactory: (): Clock => ({ now: () => Date.now() }) }] })
class Scheduler {
    constructor(@Inject(CLOCK) private clock: Clock) {}
}

Swapping the implementation - a fixed clock in tests, for example - is a one-line change to the provider, with no change to Scheduler.

Avoiding the allocation

new InjectionToken(...) is a top-level constructor call - a module-load side effect. When you would rather not allocate a token, two side-effect-free alternatives exist:

  • A class is already its own token. Class dependencies need no InjectionToken at all - just @Inject the class.
  • An abstract class tokenises an interface. A class declaration executes nothing at import and is tree-shakeable, yet it carries a type - so it serves as both the interface and its key, with no new:
ts
abstract class Clock {
    abstract now(): number;
}

@Injectable()
class SystemClock extends Clock {
    now(): number { return Date.now(); }
}

@Injectable({ providers: [{ provide: Clock, useClass: SystemClock }] })
class Scheduler {
    constructor(@Inject(Clock) private clock: Clock) {}
}

String and symbol tokens

The lightest key of all is a plain string (or symbol) - a string literal allocates nothing and needs no module-level declaration:

ts
@Injectable({ providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }] })
class ApiClient {
    constructor(@Inject('API_URL') private url: string) {}
}

inject(ApiClient); // url === 'https://api.example.com'

String and class tokens mix freely in one class - each parameter is resolved independently by its own token:

ts
@Injectable()
class Logger {}

@Injectable({
    providers: [
        { provide: 'SQL',      useValue: 'SELECT * FROM users' },
        { provide: 'MAX_CONN', useValue: 10 }
    ]
})
class Database {
    constructor(
        @Inject(Logger)     public logger: Logger, // class token -> self-injects
        @Inject('SQL')      public sql: string,     // string token -> useValue
        @Inject('MAX_CONN') public max: number      // string token -> useValue
    ) {}
}

A missing string token throws the same way a token does:

ts
@Injectable()
class Service {
    constructor(@Inject('MISSING') private value: string) {}
}

inject(Service); // throws: No provider for MISSING

The trade-off: a string key carries no value type (the useValue type is not tied to the parameter), so name it uniquely. Because each class's providers is its own registry, a key only has to be unique within that one class - collisions stay local. When you want the value type carried along, use an InjectionToken<T> instead.

Reach for InjectionToken when the dependency is a primitive or a plain config object that has no natural class to stand in for it, and you want its value type carried on the token.

Missing providers

Unlike a class token, an InjectionToken cannot self-resolve. Requesting one that has no provider throws:

ts
const MISSING = new InjectionToken<string>('MISSING');

@Injectable()
class Service {
    constructor(@Inject(MISSING) private value: string) {}
}

inject(Service); // throws: No provider for InjectionToken(MISSING)

Released under the Mozilla Public License 2.0