Day 45: Dependency Injection
Dharan Ganesan
Posted on September 11, 2023
๐ค What is Dependency Injection?
Dependency injection is a design pattern that helps manage the dependencies of a class. Instead of a class creating its own dependencies, it receives them from an external source, typically at runtime. This makes your code more modular, testable, and maintainable.
In TypeScript, we often use classes and constructors to define dependencies. But with decorators, we can simplify this process.
Example
@Injectable()
class Database {
getData() {
return 'Data from the database';
}
}
@Injectable()
class Logger {
log(message: string) {
console.log(message);
}
}
@Injectable()
class AppService {
constructor(
@Inject('Database') public database: Database,
@Inject('Logger') public logger: Logger
) {}
fetchDataAndLog() {
const data = this.database.getData();
this.logger.log(data);
}
}
Here, AppService
has two dependencies, Database
and Logger
. The @Injectable()
decorator marks this class as injectable, and the @Inject()
decorator is used to specify the dependencies.
๐ Implementing Dependency Injection
Let's break down how to achieve this without relying on any external library.
Step 1: Create Injectable Decorator
We need to create an @Injectable()
decorator. Decorators are simply functions that modify the behavior of classes, methods, or properties.
function Injectable() {
return (target: any) => {
// Some logic to handle injection
};
}
Step 2: Create Inject Decorator
Similarly, we'll create an @Inject()
decorator to specify dependencies.
function Inject(name: string) {
return (target: any, key: string) => {
// Some logic to handle injection
};
}
Step 3: Implement Dependency Injection Logic
Now, inside these decorators, you can implement the logic for injecting dependencies. For simplicity, let's use a global object to store our dependencies.
// // Define a container to hold the dependencies
class Container {
private dependencies: Map<string, any> = new Map();
register(name: string, dependency: any) {
this.dependencies.set(name, dependency);
}
resolve<T>(name: string): T {
if (!this.dependencies.has(name)) {
throw new Error(`Dependency '${name}' not registered.`);
}
// Instantiate the class when resolving it
const ClassToResolve = this.dependencies.get(name);
return new ClassToResolve();
}
}
// DI container
const container = new Container();
function Injectable() {
return (target: { new (...args: any): any }) => {
const wrapped = class extends target {
constructor(...args: any) {
const injections = (target as any).injections || [];
const injectedArgs: any[] = injections.map(({ key }) => {
console.log(`Injecting an instance identified by key ${key}`);
return container.resolve(key);
});
super(...injectedArgs);
}
};
container.register(target.name, wrapped);
return wrapped;
};
}
function Inject(name: string) {
return function (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
target['injections'] = [
{ index: parameterIndex, key: name },
...((target as any)?.injections || []),
];
return target;
};
}
๐งช Benefits of Dependency Injection
Using decorators for dependency injection in TypeScript provides several benefits:
Cleaner Code: Your classes are more focused on their primary responsibilities, making the code easier to read and maintain.
Testability: It becomes effortless to replace real dependencies with mock objects for testing.
Modularity: You can easily swap out implementations of dependencies by changing the injection configuration.
Centralized Configuration: All dependencies are defined in one place, making it easier to manage and understand the application's structure.
In a real-world application, you would likely use a DI container library like InversifyJS
or tsyringe
to manage and inject dependencies automatically.
Posted on September 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.