DEV Community

Dharan Ganesan
Dharan Ganesan

Posted on • Updated on

Day 45: Dependency Injection

๐Ÿค” 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช 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.

Top comments (1)

Collapse
 
dhrn profile image
Dharan Ganesan • Edited

Home Work
Prototype chain is currently tampered, could we avoid that?

Note: Here is the stackblitz to play