Damián Pumar

← Back to blog

Published on January 1, 2024 by Damián Pumar

What is dependencies injection? 💉

Dependencies injection is a design pattern that is used to create objects dependent on other objects. Instead of creating dependent objects within the class that uses them, we pass them through the constructor or a method.

What is an dependencies injection container? 📦

A dependency injection container is a component of our application that knows how to create and inject dependencies. The dependency injection container is responsible for instantiating classes, resolving their dependencies, and providing an instance of the class when requested.

Why use a dependency injection container? 🤔

The main reason to use a dependency injection container is to reduce the coupling between classes. The dependency injection container is responsible for creating and injecting dependencies, so the class that uses them does not need to know how to create them.

Advantages 👌

In short, using dependency injection and particularly using a dependency injection container allows us to create more flexible, maintainable and testable applications.

Disadvantages 👎

How to implement a dependency injection container in frontend? 🤔

In frontend we have many options to implement a dependency injection container, in this post we will see how to implement it in a Typescript application, but you can implement it in any frontend framework or library, or in vanilla javascript or even on any backend application.

For this occasion we will use ts-injecty a dependency injection container that I have created myself, but you can use any other dependency injection container.

ts-injecty 💉

Take a look 🙌 https://github.com/damianpumar/ts-injecty

Advantages of ts-injecty 👌

Installation 📦

npm install ts-injecty --save

Index 📖

Instantiate objects without dependencies

class A {
  public sum(x: number, y: number) {
    return x + y;
  }
}

const dependencies = [register(A).build()];

Container.register(dependencies);

const a = Container.resolve(A);

expect(a.sum(1, 2)).toBe(3);

Instantiate objects with one dependencies

Now let’s see how we can inject an object that depends on another object, in this case A depends on B. With ts-injecty we can do it in the following way, super simple 👇.

class A {
  constructor(private b: B) {}

  public sum(x: number, y: number) {
    return this.b.sum(x, y);
  }
}

class B {
  public sum(x: number, y: number) {
    return x + y;
  }
}

Container.register([register(A).withDependency(B).build()]);

const resolved = Container.resolve(ClassWithInterfaceDependency);

expect(resolved.doSomething()).toBe("HI");

Instantiate objects with dependencies of dependencies

In this case A depends on B and B depends on C, as we can dynamically inject an object that depends on another object that in turn depends on another object, easy 👇.

class A {
  constructor(private b: B) {}

  public sum(x: number, y: number) {
    return this.b.sum(x, y);
  }
}

class B {
  constructor(private c: C) {}

  public sum(x: number, y: number) {
    return this.c.sum(x, y);
  }
}

class C {
  public sum(x: number, y: number) {
    return x + y;
  }
}

const dependencies = [
  register(B).withDependency(C).build(),
  register(A).withDependency(B).build(),
];

Container.register(dependencies);

const a = Container.resolve(A);

expect(a.sum(1, 2)).toBe(3);

Instantiate objects with more than one dependency

In this case A depends on B and C, let’s see it 👇.

class A {
  constructor(
    private b: B,
    private c: C
  ) {}

  public sum(x: number, y: number) {
    return this.b.sum(x, y) + this.c.sum(x, y);
  }
}

class B {
  public sum(x: number, y: number) {
    return x + y;
  }
}

class C {
  public sum(x: number, y: number) {
    return x + y;
  }
}

const dependencies = [register(A).withDependencies(B, C).build()];

Container.register(dependencies);

const a = Container.resolve(A);

expect(a.sum(1, 2)).toBe(6);

As you can see, ts-injecty has changed the withDependency method to withDependencies in plural, this is because now we can inject more than one dependency. ts-injecty is smart enough to know that if we pass a single argument to withDependencies it is because we want to inject a single dependency, and if we pass more than one argument it is because we want to inject more than one dependency. In addition, this method will show us in the intellisense the type of argument that we need to pass applying type-checking at compile time.

Instantiate objects with a singleton dependency

ts-injecty by default registers dependencies as transient unless we register an implementation (we will see later).

Let’s see the case where A depends on B and B is a singleton dependency 👇.

class A {
  constructor(private b: B) {}

  public sum(x: number, y: number) {
    return this.b.sum(x, y);
  }
}

class B {
  private lastSum: number = 0;

  public sum(x: number, y: number) {
    this.lastSum += x + y;

    return this.lastSum;
  }
}

const dependencies = [
  register(B).asSingleton().build(),
  register(A).withDependency(B).build(),
];

Container.register(dependencies);

const a = Container.resolve(A);

expect(a.sum(1, 2)).toBe(3);

const a2 = Container.resolve(A);

expect(a2.sum(1, 2)).toBe(6); // 3 + 3

Instantiate objects with a interface dependency

As you know, in typescript interfaces are not transpiled, so this scenario is much simpler than you imagine, since it does not matter if the implementation of that interface is a function, a class or a react hook, ts-injecty has no problem injecting a dependency of an interface, we just need to comply with the contract agreed in the interface.

Let’s see a first implementation of an interface 👇.

interface B {
  doSomething(): string;
}

class A {
  constructor(private b: B) {}

  public doSomething() {
    return this.b.doSomething();
  }
}

const Implementation = () => {
  return {
    doSomething(): string {
      return "HI";
    },
  };
};

Container.register([register(A).withDependency(Implementation).build()]);

const resolved = Container.resolve(A);

expect(resolved).toBeInstanceOf(A);
expect(resolved.doSomething()).toBe("HI");

We could also solve this same case, when we have a class that implements the interface, the result is the same, what changes is the configuration of the dependency.

interface B {
  doSomething(): string;
}

class A {
  constructor(private b: B) {}

  public doSomething() {
    return this.b.doSomething();
  }
}

class Implementation implements B {
  public doSomething(): string {
    return "HI";
  }
}

Container.register([register(A).withDependency(Implementation).build()]);

const resolved = Container.resolve(A);

expect(resolved).toBeInstanceOf(A);
expect(resolved.doSomething()).toBe("HI");

I imagine that with all these alternatives, you already have an idea of how you can implement a dependency injection container in your frontend project.

Now, what would happen if you need to change your dependencies at runtime? with ts-injecty it is super simple, the truth is, since you only have to create a class inherit this class to a Factory that is implemented in ts-injecty and then register that class as a dependency, let’s see it 👇.

interface X {
  doSomething(): string;
}

class A {
  public doSomething() {
    return "HI";
  }
}

class B {
  public doSomething() {
    return "BYE";
  }
}

import { Factory } from "ts-injecty";

class FactoryImplementation extends Factory {
  public create(type: "A" | "B"): X {
    if (type === "A") {
      return this.resolver.resolve(A);
    }

    return this.resolver.resolve(B);
  }
}

Container.register([
  register(A).build(),
  register(B).build(),
  register(FactoryImplementation).build(),
]);

const resolved = Container.resolve(FactoryImplementation);

expect(resolved.create("A")).toBeInstanceOf(A);
expect(resolved.create("B")).toBeInstanceOf(B);

In this case, ts-injecty will automatically inject a Resolver when instantiating this FactoryImplementation that will allow us to resolve the dependencies we need at runtime.

If you liked this post, do not forget to share it with your friends and follow me in Twitter to be aware of my next posts.

Aquí comparto contigo un proyecto de cómo implementar ts-injecty en un proyecto de React, Stackblitz

Bye 🙌

Written by Damián Pumar

Something wrong? Let me know 🙏

← Back to blog

Recent Blogposts

  • Communication between <iframe>

    Communication between <iframe>

    The iframe's are a powerful tool, but sometimes we need to send data from the iframe to the parent window, in this post I will explain how can you send data from <iframe> to its parent window without any cors restriction and sending complex objects.

  • Add new functionality with less code ⌨️

    Add new functionality with less code ⌨️

    In this post I will explain how can you add it new functionality with less changes are posible, without modifying the design of your application, without breaking any SOLID principles using the decorator pattern.

  • Reuse Vue component in a directive ♻️

    Reuse Vue component in a directive ♻️

    In this post I will show you how to reuse a vue component in a directive without duplicate code or create vanilla javascript replicating this behavior. Sometimes it is necessary to do it because you will have to rewrite all the vue code in vanilla javascript, and sometimes that would be a lot of work, right?