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 extendSingleTokenis a concrete implementation for simple dependency keysGroupAliasTokenextendsInjectionTokenfor grouping multiple implementations (resolves to array)SingleAliasTokenextendsInjectionTokenfor grouping implementations (resolves to single instance)
The select Utility
The select utility is a convenient helper for creating tokens and accessing container features in a type-safe way. It provides shortcuts for common token operations and is the recommended way to work with tokens in your application.
select.token()
Creates a SingleToken for resolving dependencies by key. Use it with the @inject decorator:
@inject(s.token('ILogger'))- Basic usage@inject(s.token('ILogger').args(...))- With arguments@inject(s.token('Service').lazy())- With lazy loading
select.alias()
Creates a GroupAliasToken for resolving multiple implementations. Useful for plugin systems and middleware:
const token = s.alias('IMiddleware')@inject(token) middleware: IMiddleware[]- Injects array of all implementations
select.instances()
Creates a GroupInstanceToken to filter container instances by predicate:
@inject(s.instances(predicate))- Filter by predicate function@inject(s.instances())- Get all instances
select.scope.current
Injects the current container scope. Use this to access the container within your classes:
@inject(s.scope.current) scope: IContainer
select.scope.create()
Creates a new child scope on resolution. The new scope is created each time the dependency is resolved:
@inject(s.scope.create({ tags: ['request'] })) requestScope: IContainer
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 'ts-ioc-container';
/**
* User Management Domain - Type-Safe Tokens
*
* Tokens provide type-safe dependency resolution without magic strings.
* Benefits:
* - Compile-time type checking for resolved dependencies
* - IDE autocomplete and refactoring support
* - Self-documenting dependency keys
* - Prevents typos in dependency keys
*
* SingleToken<T> resolves a single dependency of type T.
* Use it when you want type safety at the injection point.
*/
interface ILogger {
log(message: string): void;
}
// Type-safe token - ensures resolved value is ILogger
const ILoggerToken = new SingleToken<ILogger>('ILogger');
class Logger implements ILogger {
log(message: string) {
console.log(message);
}
}
class App {
// Token provides type safety - logger is guaranteed to be ILogger
constructor(@inject(ILoggerToken) public logger: ILogger) {}
}
describe('SingleToken', function () {
it('should resolve dependency with type safety using SingleToken', function () {
const container = new Container({ tags: ['application'] }).addRegistration(
R.fromClass(Logger).bindToKey('ILogger'),
);
const app = container.resolve(App);
// Type-safe access to logger methods
app.logger.log('Application started');
expect(app.logger).toBeInstanceOf(Logger);
expect(app.logger).toBeDefined();
});
});
import { SingleToken, Container, inject, Registration as R } from 'ts-ioc-container';
/**
* User Management Domain - Type-Safe Tokens
*
* Tokens provide type-safe dependency resolution without magic strings.
* Benefits:
* - Compile-time type checking for resolved dependencies
* - IDE autocomplete and refactoring support
* - Self-documenting dependency keys
* - Prevents typos in dependency keys
*
* SingleToken<T> resolves a single dependency of type T.
* Use it when you want type safety at the injection point.
*/
interface ILogger {
log(message: string): void;
}
// Type-safe token - ensures resolved value is ILogger
const ILoggerToken = new SingleToken<ILogger>('ILogger');
class Logger implements ILogger {
log(message: string) {
console.log(message);
}
}
class App {
// Token provides type safety - logger is guaranteed to be ILogger
constructor(@inject(ILoggerToken) public logger: ILogger) {}
}
describe('SingleToken', function () {
it('should resolve dependency with type safety using SingleToken', function () {
const container = new Container({ tags: ['application'] }).addRegistration(
R.fromClass(Logger).bindToKey('ILogger'),
);
const app = container.resolve(App);
// Type-safe access to logger methods
app.logger.log('Application started');
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 'ts-ioc-container';
/**
* Plugins - Group Alias Token
*
* Group tokens allow you to collect multiple implementations of an interface.
* This is the basis for plugin architectures.
*
* Common use cases:
* - Event Handlers: Register multiple listeners for an event
* - Validators: Run a chain of validation rules
* - Middlewares: Execute a pipeline of request processors
* - Features: Collect all available features to display in UI
*/
const IValidatorToken = new GroupAliasToken('IValidator');
interface IValidator {
validate(input: string): boolean;
name: string;
}
@register(bindTo(IValidatorToken))
class LengthValidator implements IValidator {
name = 'Length';
validate(input: string): boolean {
return input.length >= 3;
}
}
@register(bindTo(IValidatorToken))
class FormatValidator implements IValidator {
name = 'Format';
validate(input: string): boolean {
return /^[a-zA-Z]+$/.test(input);
}
}
class FormService {
// Inject ALL registered validators as an array
constructor(@inject(IValidatorToken) public validators: IValidator[]) {}
isValid(input: string): boolean {
return this.validators.every((v) => v.validate(input));
}
getFailedValidators(input: string): string[] {
return this.validators.filter((v) => !v.validate(input)).map((v) => v.name);
}
}
describe('GroupAliasToken', function () {
it('should inject all registered plugins/validators', function () {
const container = new Container()
.addRegistration(R.fromClass(LengthValidator))
.addRegistration(R.fromClass(FormatValidator));
const formService = container.resolve(FormService);
// Valid input
expect(formService.isValid('abc')).toBe(true);
// Invalid length
expect(formService.isValid('ab')).toBe(false);
expect(formService.getFailedValidators('ab')).toContain('Length');
// Invalid format
expect(formService.isValid('123')).toBe(false);
expect(formService.getFailedValidators('123')).toContain('Format');
});
});
import { bindTo, Container, GroupAliasToken, inject, register, Registration as R } from 'ts-ioc-container';
/**
* Plugins - Group Alias Token
*
* Group tokens allow you to collect multiple implementations of an interface.
* This is the basis for plugin architectures.
*
* Common use cases:
* - Event Handlers: Register multiple listeners for an event
* - Validators: Run a chain of validation rules
* - Middlewares: Execute a pipeline of request processors
* - Features: Collect all available features to display in UI
*/
const IValidatorToken = new GroupAliasToken('IValidator');
interface IValidator {
validate(input: string): boolean;
name: string;
}
@register(bindTo(IValidatorToken))
class LengthValidator implements IValidator {
name = 'Length';
validate(input: string): boolean {
return input.length >= 3;
}
}
@register(bindTo(IValidatorToken))
class FormatValidator implements IValidator {
name = 'Format';
validate(input: string): boolean {
return /^[a-zA-Z]+$/.test(input);
}
}
class FormService {
// Inject ALL registered validators as an array
constructor(@inject(IValidatorToken) public validators: IValidator[]) {}
isValid(input: string): boolean {
return this.validators.every((v) => v.validate(input));
}
getFailedValidators(input: string): string[] {
return this.validators.filter((v) => !v.validate(input)).map((v) => v.name);
}
}
describe('GroupAliasToken', function () {
it('should inject all registered plugins/validators', function () {
const container = new Container()
.addRegistration(R.fromClass(LengthValidator))
.addRegistration(R.fromClass(FormatValidator));
const formService = container.resolve(FormService);
// Valid input
expect(formService.isValid('abc')).toBe(true);
// Invalid length
expect(formService.isValid('ab')).toBe(false);
expect(formService.getFailedValidators('ab')).toContain('Length');
// Invalid format
expect(formService.isValid('123')).toBe(false);
expect(formService.getFailedValidators('123')).toContain('Format');
});
});
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 'ts-ioc-container';
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 'ts-ioc-container';
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 'ts-ioc-container';
/**
* Configuration - Token with Arguments
*
* Sometimes you want to pass configuration values directly at the injection point.
* This is useful for:
* - Reusable services that need different config in different places
* - Configuring generic classes (like generic repositories or API clients)
* - Passing primitive values (timeouts, URLs, flags) without creating dedicated config classes
*
* The `.args()` method on a token allows you to "curry" arguments for the provider.
*/
interface IApiClient {
get(endpoint: string): string;
}
class ApiClient implements IApiClient {
constructor(
public baseUrl: string,
public timeout: number,
) {}
get(endpoint: string): string {
return `GET ${this.baseUrl}${endpoint} (timeout: ${this.timeout})`;
}
}
// Token for IApiClient
const ApiClientToken = new SingleToken<IApiClient>('IApiClient');
class DataService {
constructor(
// Inject ApiClient configured for the 'data' service
@inject(ApiClientToken.args('https://data.api.com', 5000))
public client: IApiClient,
) {}
}
class UserService {
constructor(
// Inject ApiClient configured for the 'users' service
@inject(ApiClientToken.args('https://users.api.com', 1000))
public client: IApiClient,
) {}
}
describe('Token Argument Binding', function () {
it('should create different instances based on token arguments', function () {
const container = new Container().addRegistration(R.fromClass(ApiClient).bindToKey('IApiClient'));
const dataService = container.resolve(DataService);
const userService = container.resolve(UserService);
expect(dataService.client.get('/items')).toBe('GET https://data.api.com/items (timeout: 5000)');
expect(userService.client.get('/me')).toBe('GET https://users.api.com/me (timeout: 1000)');
});
});
import { Container, inject, Registration as R, SingleToken } from 'ts-ioc-container';
/**
* Configuration - Token with Arguments
*
* Sometimes you want to pass configuration values directly at the injection point.
* This is useful for:
* - Reusable services that need different config in different places
* - Configuring generic classes (like generic repositories or API clients)
* - Passing primitive values (timeouts, URLs, flags) without creating dedicated config classes
*
* The `.args()` method on a token allows you to "curry" arguments for the provider.
*/
interface IApiClient {
get(endpoint: string): string;
}
class ApiClient implements IApiClient {
constructor(
public baseUrl: string,
public timeout: number,
) {}
get(endpoint: string): string {
return `GET ${this.baseUrl}${endpoint} (timeout: ${this.timeout})`;
}
}
// Token for IApiClient
const ApiClientToken = new SingleToken<IApiClient>('IApiClient');
class DataService {
constructor(
// Inject ApiClient configured for the 'data' service
@inject(ApiClientToken.args('https://data.api.com', 5000))
public client: IApiClient,
) {}
}
class UserService {
constructor(
// Inject ApiClient configured for the 'users' service
@inject(ApiClientToken.args('https://users.api.com', 1000))
public client: IApiClient,
) {}
}
describe('Token Argument Binding', function () {
it('should create different instances based on token arguments', function () {
const container = new Container().addRegistration(R.fromClass(ApiClient).bindToKey('IApiClient'));
const dataService = container.resolve(DataService);
const userService = container.resolve(UserService);
expect(dataService.client.get('/items')).toBe('GET https://data.api.com/items (timeout: 5000)');
expect(userService.client.get('/me')).toBe('GET https://users.api.com/me (timeout: 1000)');
});
});
Lazy Loading
Tokens support lazy loading, deferring instantiation until access:
import { args, Container, inject, Registration as R, SingleToken } from 'ts-ioc-container';
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 'ts-ioc-container';
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 'ts-ioc-container';
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 'ts-ioc-container';
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);
});
});
Additional Token Types
The library provides several specialized token types for advanced use cases. These tokens are typically used internally by the framework but can also be used directly when needed.
ClassToken
ClassToken wraps a class constructor and supports argument binding, lazy loading, and custom argument functions. It’s primarily used internally when resolving classes directly.
Usage: new ClassToken(MyService)
Supports: args(), argsFn(), lazy()
FunctionToken
FunctionToken wraps a custom resolution function. This is used by select.scope.current and select.scope.create() to inject container scopes. The function receives the container and returns the resolved value.
Note: Does not support args(), argsFn(), or lazy() methods (throws MethodNotImplementedError).
ConstantToken
ConstantToken always returns a constant value. It’s used internally by the @inject decorator to pass literal values. The token ignores the container and always returns the same value.
Note: Does not support args(), argsFn(), or lazy() methods.
GroupInstanceToken
GroupInstanceToken filters container instances by a predicate function. This is used by select.instances() to query instances. It supports cascade control to include or exclude parent scope instances.
Usage:
new GroupInstanceToken(predicate)- Filter instancestoken.cascade(false)- Current scope only
Note: Does not support args(), argsFn(), or lazy() methods.