Dependency injection in Typescript

JavaScript/TypeScript

Dependency Injection is one of important techniques to develop testable software. This technique which passes the dependency from outside of the class is I think basic technique to write test and extendable class. If a class instantiates the dependency directly by using new keyword it’s not possible to inject fake object to the target class which you want to write unit test. By applying this technique, you can also loose coupling to other components which is one of good habits.

Sponsored links

Code to make it better

Following example can often be my first implementation in order to check the basic implementation. 

// Person.ts
export class Person {
    private ability = new Ability();

    public calculatePoint(): number {
        const rank = this.ability.getAbility();
        const base = 5;
        let additionalPoint = 0;
        if (rank === Rank.A) {
            additionalPoint = 10;
        } else if (rank === Rank.B) {
            additionalPoint = 5;
        }
        return base + additionalPoint;
    }
}
// Person_spec.ts
describe("Person - original", () => {
    let ability: Ability;
    let fakeTimer: sinon.SinonFakeTimers;
    beforeEach(() => {
        ability = new Ability();
    });
    afterEach(() => {
        fakeTimer.restore();
    });

    describe("calculatePoint", () => {
        it("should return 15 when Rank is A", () => {
            const fakeTime = new Date(2020, 10, 10, 9);
            fakeTimer = sinon.useFakeTimers({ now: fakeTime });
            sinon.stub(ability).getAbility.returns(Rank.A);
            const instance = new Person();
            const result = instance.calculatePoint();
            expect(result).to.equal(15);
        });
    });
});

But there is no seam to pass the dependency in this example which means you have to use actual Assessment class that may require complicated setup to get desired value. Ability class is not complicated and we can write test by using sinon.useFakeTimer() but this code tests combination of Ability and Person class. If you modify Ability class the test may fail even though you don’t make any change to Person class. We should somehow move the instantiation from this class to other class and pass the instance to Person class. There are several ways to do this but what I often apply is following.

  • Constructor Injection
  • Setter Injection
  • Factory Method

Factory Method may not be categorized in Dependency Injection but it can solve the same problem.

Constructor Injection

This is suitable when you need to determine which class to use according to input value and you don’t have to change the dependent instance after creating the class (Person class here). 

// Person.ts
export class Person {
    constructor(private ability: Ability) { }
    public calculatePoint(): number {
        const rank = this.ability.getAbility();
        const base = 5;
        let additionalPoint = 0;
        if (rank === Rank.A) {
            additionalPoint = 10;
        } else if (rank === Rank.B) {
            additionalPoint = 5;
        }
        return base + additionalPoint;
    }
}
// Person_spec.ts
describe("Person - constructor", () => {
    let ability: Ability;
    beforeEach(() => {
        ability = new Ability();
    })
    describe("calculatePoint", () => {
        it("should return 15 when Rank is A", () => {
            sinon.stub(ability).getAbility.returns(Rank.A);
            const instance = new Person(ability);
            const result = instance.calculatePoint();
            expect(result).to.equal(15);
        });
    });
});

Setter Injection

This is suitable when you need to update the dependent instance while running the program. This way requires null check in each function because it is undefined unless the value is set via the setter. If it’s not necessary to update the instance I apply Constructor Injection because of the null check. Additionally, unnecessary seam can cause a problem. For example, one developer assigns an instance somewhere in order to make sure that the instance is assigned before function call whereas other developer has already assigned a different instance. In this case, some data stored in the dependent instance is discarded and target function may not work as expected. It can happen when the developer has not been familiar with the project.

// Person.ts
export class Person {
    private _ability?: Ability

    public set ability(value: Ability) {
        this._ability = value;
    }

    public calculatePoint(): number {
        if (!this._ability) {
            throw new Error("ability instance is undefined.");
        }
        const rank = this._ability.getAbility();
        const base = 5;
        let additionalPoint = 0;
        if (rank === Rank.A) {
            additionalPoint = 10;
        } else if (rank === Rank.B) {
            additionalPoint = 5;
        }
        return base + additionalPoint;
    }
}
// Person_spec.ts
describe("Person - setter", () => {
    let ability: Ability;
    beforeEach(() => {
        ability = new Ability();
    });
    describe("calculatePoint", () => {
        it("should throw an error when ability is undefined", () => {
            sinon.stub(ability).getAbility.returns(Rank.A);
            const instance = new Person();
            const result = () => instance.calculatePoint();
            expect(result).to.throw("ability instance is undefined")
        });
    });
});

Factory Method

I often create factory class because a caller doesn’t have to know which class to pass in many cases. In this example, Ability class is only one class to get rank. Person class knows which class to use but if Person class instantiates the class we go back to the start point. Therefore we should create Factory class and return new Ability instance in create function. By doing this, you can replace actual implementation with fake object by sinon. You can also pass config parameter from constructor to factory class if some parameters are necessary for setup. Function should be in either a class or object since it is not possible to replace top level exported function.


// AbilityFactory.ts
export class AbilityFactory {
    public static create(): Ability {
        return new Ability();
    }
}
// Person.ts
export class Person {
    private ability: Ability
    constructor() {
        this.ability = AbilityFactory.create();
    }
    public calculatePoint(): number {
        const rank = this.ability.getAbility();
        const base = 5;
        let additionalPoint = 0;
        if (rank === Rank.A) {
            additionalPoint = 10;
        } else if (rank === Rank.B) {
            additionalPoint = 5;
        }
        return base + additionalPoint;
    }
}
// Person_spec.ts
describe("Person - factory", () => {
    let ability: Ability;
    let stubFactory: sinon.SinonStub;
    beforeEach(() => {
        ability = new Ability();
        stubFactory = sinon.stub(AbilityFactory, "create");
        stubFactory.returns(ability);
    });
    afterEach(() => {
        stubFactory.restore();
    });

    describe("calculatePoint", () => {
        it("should return 15 when Rank is A", () => {
            sinon.stub(ability).getAbility.returns(Rank.A);
            const instance = new Person();
            const result = instance.calculatePoint();
            expect(result).to.equal(15);
        });    });
});

Other ways

DI container 

I have a little experience with DI container. I have used MEF in C# when I didn’t know about clear architecture well. But I haven’t had any issues so far for DI without DI Container. I think using DI container requires learning cost and it is not intuitive. We can simply inject dependencies without it. But if I use it and learn something I will write something about it.

Parameter Injection

I’m not sure if the word exists. You can pass the dependency by specifying it in function parameter. I’ve never used this technique but I guess it can be applied when calling the target function frequently with different instance.

Conclusion

I explained 3 ways to inject dependency which I often apply. I explained Factory Method to use it in constructor but it can also be used in a function. I have never seen it in my career though it depends on the specification.

  • Constructor Injection
    when you need to assign instance only once and the instance needs to be determined according to input value
  • Setter Injection
    when you need to assign instance multiple times
  • Factory Method
    when you need to assign instance only once and the instance doesn’t need to be determined outside of the class. 

Complete source code can be found here.

BlogPost/src/HowToInjectDependencies at master · yuto-yuto/BlogPost
Contribute to yuto-yuto/BlogPost development by creating an account on GitHub.

Comments

Copied title and URL