Day 45: Dependency Injection
Tags: javascript, typescript, 100daysofcode, day45
Published: 2023-09-11 14:00:00

## 🤔 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

```typescript
@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. 

```typescript
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.

```typescript
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.

```typescript
// // 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.