Implement Angular's inject Function
Angular's dependency injection system is a cornerstone of its architecture. The inject function provides a powerful and flexible way to access dependencies within Angular contexts. This challenge tasks you with implementing a simplified version of Angular's inject function to understand its core mechanics.
Problem Description
You need to create a TypeScript function named inject that mimics the behavior of Angular's inject function. This function should allow you to retrieve an instance of a service or token from a provided dependency injection (DI) container.
Key Requirements:
- The
injectfunction should accept a token (which can be a class or a string identifier) as an argument. - It should retrieve and return an instance of the corresponding service from a DI container.
- If the token is not found in the container, it should throw an error.
- You'll need to simulate a basic DI container that can store and retrieve these dependencies.
Expected Behavior:
When inject(SomeService) is called, and SomeService has been registered in the DI container, the inject function should return the singleton instance of SomeService. If it's called again for the same service, it should return the same instance.
Edge Cases to Consider:
- What happens if
injectis called for a token that hasn't been registered? - How would you handle the creation of service instances if they have dependencies themselves (for this simplified version, assume services don't have constructor dependencies that need injection)?
Examples
Example 1:
// Define a simple service
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
// --- Your inject implementation would go here ---
// Simulate a DI container and register the service
const container = new Map<any, any>();
container.set(LoggerService, new LoggerService()); // Registering as a singleton
// Using your inject function
const logger = inject(LoggerService); // Assuming inject uses the 'container' internally
logger.log("Hello from Logger!");
Expected Output (to console):
[LOG] Hello from Logger!
Explanation:
The inject function, when called with LoggerService, looks up LoggerService in the container and returns the pre-existing instance. This instance's log method is then called.
Example 2:
// Define another service
class UserService {
getUserName(): string {
return "Alice";
}
}
// --- Your inject implementation would go here ---
// Assume LoggerService is already registered as in Example 1
// Registering UserService
container.set(UserService, new UserService());
// Injecting both services
const logger = inject(LoggerService);
const user = inject(UserService);
logger.log(`User: ${user.getUserName()}`);
Expected Output (to console):
[LOG] User: Alice
Explanation:
Both LoggerService and UserService are retrieved via inject from the container and their methods are used.
Example 3 (Error Case):
// Assume LoggerService and UserService are registered
// Attempting to inject a service that hasn't been registered
class AnalyticsService {
trackEvent(event: string) {
console.log(`Tracking: ${event}`);
}
}
// --- Your inject implementation would go here ---
// Using your inject function for an unregistered service
try {
const analytics = inject(AnalyticsService); // This should throw an error
} catch (e: any) {
console.error(e.message);
}
Expected Output (to console):
No provider for AnalyticsService!
Explanation:
Since AnalyticsService was never added to the container, calling inject with it results in an error.
Constraints
- Your
injectfunction should be written in TypeScript. - The DI container can be a simple JavaScript
Mapor a similar data structure. - For this challenge, assume that services do not have constructor dependencies that require injection themselves. They are created directly when registered.
- The
injectfunction should return a singleton instance. Subsequent calls for the same token should return the exact same instance.
Notes
- Consider how you will manage the DI container. Should it be a global variable, passed to
inject, or managed within a scope? For simplicity, you might start with a global or a module-scoped container. - Think about how you will store both the token (the key) and the instance (the value) in your container.
- The error message for an unregistered token should be descriptive, similar to Angular's error messages (e.g., "No provider for [TokenName]!").
- This exercise is about understanding the core lookup and retrieval mechanism of
inject. Advanced features like factory providers,useClass,useValue, anduseFactoryare out of scope for this challenge.