Skip to main content

Understanding Dependency Identifiers

In dependency injection (DI), identifiers play a crucial role in determining how dependencies are provided and resolved. This guide explores the different types of identifiers and explains how to use them effectively with Suites.

What You'll Find in This Section

  • Class-based Injection - Using TypeScript classes as identifiers
  • Token-based Injection - Working with string and symbol tokens
  • Handling Metadata - Adding context to your identifiers
  • Framework-specific Examples - Code samples for NestJS and InversifyJS
note

The usage and support for different types of identifiers or metadata may vary between adapters. Please refer to the specific adapter's documentation for detailed information.

Before diving in, ensure you've installed the appropriate adapter for your DI framework:

$ npm i -D @suites/di.nestjs
tip

See the full installation guide here

Class-based Injection 💡

Class-based injection is a fundamental concept in Dependency Injection frameworks. It uses TypeScript classes both as a blueprint for creating instances and as an identifier for resolving dependencies.

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
class UserService {
constructor(private apiService: ApiService) {}
}

To resolve or mock a class-based dependency with Suites, simply use the class itself:

UnitReference API
const { unitRef } = await TestBed.solitary(UserService).compile();
const userApiService = unitRef.get(ApiService);
MockOverride API
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock(ApiService)
.using({ ... })
.compile();

Token-based Injection 💡

Tokens (strings or symbols) serve as unique keys to fetch specific dependencies from the container. They're especially useful for distinguishing multiple instances of the same type or for interface-based dependencies.

The concept of token-based injection is consistent across DI frameworks, but the syntax varies by implementation.

Class as Token

Sometimes, classes are explicitly used as injection tokens:

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
class UserService {
constructor(@Inject(ApiService) private apiService: ApiService) {}
}

With Suites, you can resolve or mock these dependencies using the class itself:

UnitReference API
const { unitRef } = await TestBed.solitary(UserService).compile();
const userApiService = unitRef.get(ApiService);
MockOverride API
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock(ApiService)
.using({ ... })
.compile();

String-Based / Symbol-Based Tokens

Tokens can be strings or symbols, each serving as a unique identifier for a dependency.

Consider the following Logger interface as an example:

interface Logger {
log(message: string): void;
}

And a token constant:

export const LOGGER_TOKEN = 'LOGGER_TOKEN'

You would register this token with your DI framework to associate it with a specific implementation of the Logger interface:

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
class UserService {
constructor(@Inject(LOGGER_TOKEN) private logger: Logger) {}
}

With Suites, you resolve or mock token-based dependencies using the token itself:

UnitReference API
const { unitRef } = await TestBed.solitary(UserService).compile();
const logger = unitRef.get<Logger>(LOGGER_TOKEN);
MockOverride API
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock<Logger>(LOGGER_TOKEN)
.using({ ... })
.compile();

Circular Dependencies and Lazy Loading

Circular dependencies occur when two classes depend on each other. While best avoided, some scenarios require them. DI frameworks provide mechanisms like lazy loading to handle such cases:

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
class UserService {
constructor(@Inject(forwardRef(() => ApiService)) private apiService: ApiService) {}
}

With Suites, you can resolve or mock circular dependencies using the base identifier without the circular dependency wrapper:

// Example for NestJS
const apiService = unitRef.get(ApiService); // Not forwardRef(() => ApiService)

// Example for InversifyJS
const apiService = unitRef.get(ApiService); // Not new LazyServiceIdentifer(() => ApiService)

Identifiers with Metadata 💡

Suites allows for metadata inclusion alongside the primary identifier. Metadata provides additional context or qualifiers for identifiers, enabling more precise control over dependency resolution. For instance, a service might have multiple instances, each distinguished by specific metadata.

Currently, only the InversifyJS adapter supports metadata. For more information, visit the InversifyJS documentation page.

Next Steps