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:

classDiagram class InjectionToken { +resolve(IContainer, ResolveOneOptions?) T +args(...deps) InjectionToken~T~ +argsFn(getArgsFn) InjectionToken~T~ +lazy() InjectionToken~T~ } class SingleToken { +token: string | symbol +resolve(IContainer) T +bindTo(IRegistration) void +args(...args) SingleToken~T~ +argsFn(getArgsFn) SingleToken~T~ +lazy() SingleToken~T~ } class GroupAliasToken { +token: string | symbol +resolve(IContainer) T[] +bindTo(IRegistration) void +args(...args) GroupAliasToken~T~ +argsFn(getArgsFn) GroupAliasToken~T~ +lazy() InjectionToken~T[]~ } class SingleAliasToken { +token: string | symbol +resolve(IContainer) T +bindTo(IRegistration) void +args(...args) InjectionToken~T~ +argsFn(getArgsFn) InjectionToken~T~ +lazy() InjectionToken~T~ } InjectionToken <|-- SingleToken InjectionToken <|-- GroupAliasToken InjectionToken <|-- SingleAliasToken note for GroupAliasToken "Resolves to array of T\nSupports alias grouping" note for SingleAliasToken "Resolves to single T\nSupports alias grouping"

Key Points:

  • InjectionToken is the abstract base class that all tokens extend
  • SingleToken is a concrete implementation for simple dependency keys
  • GroupAliasToken extends InjectionToken for grouping multiple implementations (resolves to array)
  • SingleAliasToken extends InjectionToken for 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.

TypeScript __tests__/readme/token.spec.ts
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.

TypeScript __tests__/readme/tokenGroupAlias.spec.ts
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.

TypeScript __tests__/readme/tokenSingleAlias.spec.ts
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:

TypeScript __tests__/readme/tokenArgs.spec.ts
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:

TypeScript __tests__/readme/tokenLazy.spec.ts
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:

TypeScript __tests__/readme/tokenArgsFn.spec.ts
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);
  });
});