Token
Tokens are used to identify and resolve dependencies in the container. The library provides several token types for different use cases, each offering different levels of type safety and flexibility.
Why Tokens?
While you can use plain strings or symbols as dependency keys, tokens provide additional benefits:
- Type Safety: Tokens carry type information, improving IntelliSense and compile-time checks
- Uniqueness: Tokens ensure unique identification, preventing accidental key collisions
- Features: Some tokens support additional features like argument binding and lazy loading
- Refactoring: Tokens make refactoring safer by providing a single source of truth
Token Hierarchy
All tokens in the library extend the abstract InjectionToken base class,
which defines the common interface for dependency resolution. The following diagram
shows the inheritance hierarchy and relationships between different token types:
Key Points:
-
InjectionTokenis the abstract base class that all tokens extend -
SingleTokenis a concrete implementation for simple dependency keys -
GroupAliasTokenextendsInjectionTokenfor grouping multiple implementations (resolves to array) -
SingleAliasTokenextendsInjectionTokenfor grouping implementations (resolves to single instance)
SingleToken
SingleToken is a simple token that wraps a string or symbol key. It
provides type safety while maintaining simplicity.
import { SingleToken, Container, inject, Registration as R } from '../../lib';
interface ILogger {
log(message: string): void;
}
const ILoggerKey = new SingleToken<ILogger>('ILogger');
class Logger implements ILogger {
log(message: string) {
console.log(message);
}
}
class App {
constructor(@inject(ILoggerKey) public logger: ILogger) {}
}
describe('SingleToken', function () {
it('should resolve using SingleToken', function () {
const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
const app = container.resolve(App);
app.logger.log('Hello');
expect(app.logger).toBeInstanceOf(Logger);
expect(app.logger).toBeDefined();
});
});
import { SingleToken, Container, inject, Registration as R } from '../../lib';
interface ILogger {
log(message: string): void;
}
const ILoggerKey = new SingleToken<ILogger>('ILogger');
class Logger implements ILogger {
log(message: string) {
console.log(message);
}
}
class App {
constructor(@inject(ILoggerKey) public logger: ILogger) {}
}
describe('SingleToken', function () {
it('should resolve using SingleToken', function () {
const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
const app = container.resolve(App);
app.logger.log('Hello');
expect(app.logger).toBeInstanceOf(Logger);
expect(app.logger).toBeDefined();
});
});
GroupAliasToken
GroupAliasToken is used to group multiple registrations under a common
alias. This is perfect for plugin systems or middleware. It resolves to an array
of all implementations bound to the alias.
import { bindTo, Container, GroupAliasToken, inject, register, Registration as R } from '../../lib';
const IMiddlewareToken = new GroupAliasToken('IMiddleware');
interface IMiddleware {
apply(): void;
}
@register(bindTo(IMiddlewareToken))
class LoggerMiddleware implements IMiddleware {
apply() {
console.log('Logger middleware');
}
}
@register(bindTo(IMiddlewareToken))
class AuthMiddleware implements IMiddleware {
apply() {
console.log('Auth middleware');
}
}
class App {
constructor(@inject(IMiddlewareToken) public middleware: IMiddleware[]) {}
run() {
this.middleware.forEach((m) => m.apply());
}
}
describe('GroupAliasToken', function () {
it('should resolve multiple implementations by alias', function () {
const container = new Container()
.addRegistration(R.fromClass(LoggerMiddleware))
.addRegistration(R.fromClass(AuthMiddleware));
const app = container.resolve(App);
app.run(); // Both middleware are applied
expect(app.middleware.length).toBe(2);
});
});
import { bindTo, Container, GroupAliasToken, inject, register, Registration as R } from '../../lib';
const IMiddlewareToken = new GroupAliasToken('IMiddleware');
interface IMiddleware {
apply(): void;
}
@register(bindTo(IMiddlewareToken))
class LoggerMiddleware implements IMiddleware {
apply() {
console.log('Logger middleware');
}
}
@register(bindTo(IMiddlewareToken))
class AuthMiddleware implements IMiddleware {
apply() {
console.log('Auth middleware');
}
}
class App {
constructor(@inject(IMiddlewareToken) public middleware: IMiddleware[]) {}
run() {
this.middleware.forEach((m) => m.apply());
}
}
describe('GroupAliasToken', function () {
it('should resolve multiple implementations by alias', function () {
const container = new Container()
.addRegistration(R.fromClass(LoggerMiddleware))
.addRegistration(R.fromClass(AuthMiddleware));
const app = container.resolve(App);
app.run(); // Both middleware are applied
expect(app.middleware.length).toBe(2);
});
});
SingleAliasToken
SingleAliasToken is similar to GroupAliasToken, but resolves
to a single instance instead of an array. This is useful when you want to use multiple interfaces for the same implementation.
import { Container, inject, Registration as R, SingleAliasToken, toSingleAlias } from '../../lib';
const ILoggerToken = new SingleAliasToken<ILogger>('ILogger');
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(message);
}
}
class FileLogger implements ILogger {
log(message: string) {
// Write to file
}
}
class App {
constructor(@inject(ILoggerToken) public logger: ILogger) {}
run() {
this.logger.log('Hello'); // Uses one of the registered loggers
}
}
describe('SingleAliasToken', function () {
it('should resolve single implementation by alias', function () {
const container = new Container()
.addRegistration(R.fromClass(ConsoleLogger).bindToKey('ConsoleLogger').bindToAlias('ILogger'))
.addRegistration(R.fromClass(FileLogger).bindToKey('FileLogger').bindToAlias('ILogger'));
const app = container.resolve(App);
app.run();
expect(app.logger).toBeDefined();
expect(app.logger.log).toBeDefined();
});
it('should support toSingleAlias helper', function () {
const ILoggerToken2 = toSingleAlias<ILogger>('ILogger');
expect(ILoggerToken2).toBeDefined();
});
});
import { Container, inject, Registration as R, SingleAliasToken, toSingleAlias } from '../../lib';
const ILoggerToken = new SingleAliasToken<ILogger>('ILogger');
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(message);
}
}
class FileLogger implements ILogger {
log(message: string) {
// Write to file
}
}
class App {
constructor(@inject(ILoggerToken) public logger: ILogger) {}
run() {
this.logger.log('Hello'); // Uses one of the registered loggers
}
}
describe('SingleAliasToken', function () {
it('should resolve single implementation by alias', function () {
const container = new Container()
.addRegistration(R.fromClass(ConsoleLogger).bindToKey('ConsoleLogger').bindToAlias('ILogger'))
.addRegistration(R.fromClass(FileLogger).bindToKey('FileLogger').bindToAlias('ILogger'));
const app = container.resolve(App);
app.run();
expect(app.logger).toBeDefined();
expect(app.logger.log).toBeDefined();
});
it('should support toSingleAlias helper', function () {
const ILoggerToken2 = toSingleAlias<ILogger>('ILogger');
expect(ILoggerToken2).toBeDefined();
});
});
Argument Binding
Tokens support argument binding, allowing you to pass arguments when resolving:
import { Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
timeout: number;
}
class ConfigService implements IConfig {
constructor(
public apiUrl: string,
public timeout: number,
) {}
}
const IConfigKey = new SingleToken<IConfig>('IConfig');
class App {
constructor(
@inject(IConfigKey.args('https://api.example.com', 5000))
public config: IConfig,
) {}
}
describe('Token Argument Binding', function () {
it('should bind arguments to token', function () {
const container = new Container().addRegistration(R.fromClass(ConfigService).bindToKey('IConfig'));
const app = container.resolve(App);
expect(app.config.apiUrl).toBe('https://api.example.com');
expect(app.config.timeout).toBe(5000);
});
});
import { Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
timeout: number;
}
class ConfigService implements IConfig {
constructor(
public apiUrl: string,
public timeout: number,
) {}
}
const IConfigKey = new SingleToken<IConfig>('IConfig');
class App {
constructor(
@inject(IConfigKey.args('https://api.example.com', 5000))
public config: IConfig,
) {}
}
describe('Token Argument Binding', function () {
it('should bind arguments to token', function () {
const container = new Container().addRegistration(R.fromClass(ConfigService).bindToKey('IConfig'));
const app = container.resolve(App);
expect(app.config.apiUrl).toBe('https://api.example.com');
expect(app.config.timeout).toBe(5000);
});
});
Lazy Loading
Tokens support lazy loading, deferring instantiation until access:
import { args, Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
}
class ConfigService implements IConfig {
constructor(public apiUrl: string) {}
}
class App {
constructor(
@inject(new SingleToken<IConfig>('IConfig').lazy())
public config: IConfig,
) {}
}
describe('Token Lazy Loading', function () {
it('should support lazy loading with tokens', function () {
const container = new Container().addRegistration(
R.fromClass(ConfigService).pipe(args('https://api.example.com')).bindToKey('IConfig'),
);
const app = container.resolve(App);
expect(app.config).toBeDefined();
expect(app.config.apiUrl).toBe('https://api.example.com');
});
});
import { args, Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
}
class ConfigService implements IConfig {
constructor(public apiUrl: string) {}
}
class App {
constructor(
@inject(new SingleToken<IConfig>('IConfig').lazy())
public config: IConfig,
) {}
}
describe('Token Lazy Loading', function () {
it('should support lazy loading with tokens', function () {
const container = new Container().addRegistration(
R.fromClass(ConfigService).pipe(args('https://api.example.com')).bindToKey('IConfig'),
);
const app = container.resolve(App);
expect(app.config).toBeDefined();
expect(app.config.apiUrl).toBe('https://api.example.com');
});
});
Dynamic Arguments
Tokens support dynamic argument resolution using functions:
import { Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
timeout: number;
}
class ConfigService implements IConfig {
constructor(
public apiUrl: string,
public timeout: number,
) {}
}
const IConfigKey = new SingleToken<IConfig>('IConfig');
class App {
constructor(
@inject(IConfigKey.argsFn((scope) => [scope.resolve('API_URL'), 5000]))
public config: IConfig,
) {}
}
describe('Token Dynamic Arguments', function () {
it('should resolve arguments dynamically', function () {
const container = new Container()
.addRegistration(R.fromValue('https://api.example.com').bindToKey('API_URL'))
.addRegistration(R.fromClass(ConfigService).bindToKey('IConfig'));
const app = container.resolve(App);
expect(app.config.apiUrl).toBe('https://api.example.com');
expect(app.config.timeout).toBe(5000);
});
});
import { Container, inject, Registration as R, SingleToken } from '../../lib';
interface IConfig {
apiUrl: string;
timeout: number;
}
class ConfigService implements IConfig {
constructor(
public apiUrl: string,
public timeout: number,
) {}
}
const IConfigKey = new SingleToken<IConfig>('IConfig');
class App {
constructor(
@inject(IConfigKey.argsFn((scope) => [scope.resolve('API_URL'), 5000]))
public config: IConfig,
) {}
}
describe('Token Dynamic Arguments', function () {
it('should resolve arguments dynamically', function () {
const container = new Container()
.addRegistration(R.fromValue('https://api.example.com').bindToKey('API_URL'))
.addRegistration(R.fromClass(ConfigService).bindToKey('IConfig'));
const app = container.resolve(App);
expect(app.config.apiUrl).toBe('https://api.example.com');
expect(app.config.timeout).toBe(5000);
});
});