Dependecy injection using decorators
In this tutorial, we will take a look at an example of how we can use decorators in TypeScript to create our own Dependecy injection implementation.
Dependency injection is basically a way of providing an object with the necessary objects or function that it requires to function properly. Many languages and frameworks are using dependecy injection, such as Java Spring, Angular JS and so on. And in this guide, we will do our own dependency injection implementation using decorators.
Task
In this tutorial, we will create two decorators. One called Injectable and another called Inject. The injectable will register the class to a Map, that our program can use to inject to class properties so we don't have to care about initializing them ourselves. And then, we can use the inject decorator to auto initialize the properties.
Then, we will create two "services" and two classes. The services will be called SalaryService and BenefitsService that will be injectable, and then we will have two classes called Employee and Boss which will inject our services. Our Inject decorator will be able handle input parameters that will be passed along to the injectables.
Config
We need to add some config to our tsconfig.json first. Since our decorators will create the instances, we need to tell TypeScript to skip the strict checks for property initialization.
{ "compilerOptions": { "strictPropertyInitialization": false } }
Injectable
Let's start with our Injectable decorator. We will basically add the injectable classes to a map, with the entity param as the key.
const injects = new Map<string, any>(); function Injectable(entity: string) { return function (target: any, context: any) { injects.set(entity, target); }; }
And now, we will create our Injectable classes that will be annotated with our newly created decorator.
type Role = "Employee" | "Boss"; @Injectable("SalaryService") class SalaryService { calculateSalary(yearsOfEmployement: number, baseSalary: number, role: Role) { return baseSalary + yearsOfEmployement * (role === "Boss" ? 50 : 5); } } @Injectable("BenefitsService") class BenefitsService { constructor(private role: Role) {} getBenefits() { if (this.role === "Boss") return "100k bonus"; return `5k bonus`; } }
As you can see, our BenefitsService needs to be provided with a role in the constructor. For our SalaryService, we choose to put it into the function directly. NOTE: that this is not best practice, I just want to show you guys that we can use the constructor using decorators as well 😉
Inject
Let's do our next decorator, which we will call Inject. This decorator will simply grab the service registered in the map, and call new() and return the initialized object. We will also provide it with a generic type for the second argument, that will be passed along to the injectable constructor.
function Inject<T>(entity: string, args: T = null) { return function (target: any, context: any) { return () => { const InjectedCtor = injects.get(entity); if (!InjectedCtor) { throw new Error(`No entity found for ${entity}`); } return new InjectedCtor(args); }; }; }
And now, we can use this decorator for our class properties for the respective services.
Employee and Boss class
Let's create our classes, that will use our dependency injection decorators. We will start with Employee class:
class Employee { constructor(public name: string) {} @Inject("SalaryService") public salaryService: SalaryService; @Inject<Role>("BenefitsService", "Employee") public benefitsService: BenefitsService; }
And our Boss class will look like this:
class Boss { constructor(public name: string) {} @Inject("SalaryService") public salaryService: SalaryService; @Inject<Role>("BenefitsService", "Boss") public benefitsService: BenefitsService; }
Testing
And now, we can create instances of our classes and call our services "without" initializing them (since our decorators are doing that for us) 🍾
const employee = new Employee("John Doe"); console.log( `Salary employee: ${employee.salaryService.calculateSalary( 5, 1000, "Employee" )}` ); console.log(employee.benefitsService.getBenefits()); console.log("************"); const boss = new Boss("John Boss"); console.log( `Salary boss: ${boss.salaryService.calculateSalary(5, 1000, "Boss")}` ); console.log(boss.benefitsService.getBenefits());
Output:
Salary employee: 1015 5k bonus ************ Salary boss: 1250 100k bonus
There we go - looks like it pays off being a boss 😔
Summary
There we have it. In this fun tutorial, we explored how we can utilize decorators to create our own dependency injection implementation in TypeScript. Please let me know what you thought of this guide on our contact us page.
Have a good one!