DEV Community

Daniel Ostrovsky
Daniel Ostrovsky

Posted on • Originally published at Medium on

Metaprogramming in JavaScript/TypeScript Part #3 (Dependency Injection)

This is the third article in the "Metaprogramming in JavaScript and TypeScript" series.

First two:

In this article, we will consider such a concept as dependency injection.

And in this example, we will try to understand the principle of Metaprogramming and get acquainted with the reflect-metadata library.

Let's start right away with an example:

Suppose we have three classes:

1) MoviesService — should return information about movies to us, preferably with comments.

2) CommentsService — works with comments.

3) CrudService — a standard service for working with API services

/**
* CRUD Service for CRUD operations against BE / DB
*/
class CrudService {
getData(entity: string) {
return `Some Data from -> ${entity}`;
}
}
/**
* Service to retrieve/crate/update comments
*/
class CommentsService {
constructor(public crudService: CrudService) {}
getComments() {
return this.crudService.getData('/comments');
}
}
/**
* Service to retrieve/crate/update comments
*/
class MoviesService {
constructor(
private commentsService: CommentsService,
private crudService: CrudService
) {}
getMovies() {
return this.crudService.getData('/movies');
}
getComments() {
return this.commentsService.getComments();
}
}

The initialization of the MoviesService class would look like this:

const movies = new MoviesService(
new CommentsService(new CrudService()),
new CrudService()
);

And it seems everything looks logical, but it's not good enough :).

Indeed, in the constructors of each one of the classes, I have already indicated which dependencies are necessary to initialize this class … so why do we need this again during initialization?

And what if, for unit testing, we need to substitute a certain CrudServiceStub instead of CrudService? There is also the issue of CrudService being SingleTone and not being initialized twice (MoviesService and CommentsService). All this, of course, can be solved in the old ways, but we are not here for this :)

Let's start with decorators.

Simple and even empty decorator

export const Injectable = (): GenericClassDecorator<Type<object>> => {
return (target: Type<object>) => {
// console.log(
// '[Injectable]',
// target.name,
// Reflect.getMetadata('design:paramtypes', target)
// );
};
};

Since the decorator is a regular function, after compilation into JS, we will get:

export const Injectable = () => {
return (target) => {
// console.log(
// '[Injectable]',
// target.name,
// Reflect.getMetadata('design:paramtypes', target)
// );
};
};

But if we use this as decorator…

export const Injectable = (): GenericClassDecorator<Type<object>> => {
return (target: Type<object>) => {
// console.log(
// '[Injectable]',
// target.name,
// Reflect.getMetadata('design:paramtypes', target)
// );
};
};
@Injectable()
class CrudService {
getData(entity: string) {
return `Some Data from -> ${entity}`;
}
}

Now JS will look completely different:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
export const Injectable = () => {
return (target) => {
// console.log(
// '[Injectable]',
// target.name,
// Reflect.getMetadata('design:paramtypes', target)
// );
};
};
let CrudService = class CrudService {
getData(entity) {
return `Some Data from -> ${entity}`;
}
};
CrudService = __decorate([
Injectable()
], CrudService);

We are interested in the __decorate functionOr rather the line:

Reflect.decorate(decorators, target, key, desc);

In order to understand what it does, let's add a console.log. To our decorator.

And the decorator itself will also be added to the CommentsService class

export const Injectable = (): GenericClassDecorator<Type<object>> => {
return (target: Type<object>) => {
console.log(
'[Injectable]',
target.name,
Reflect.getMetadata('design:paramtypes', target)
);
};
};
@Injectable()
class CrudService {
getData(entity: string) {
return `Some Data from -> ${entity}`;
}
}
@Injectable()
class CommentsService {
constructor(public crudService: CrudService) {}
getComments() {
return this.crudService.getData('/comments');
}
}
//LOG: [Injectable] CrudService undefined
//LOG: [Injectable] CommentsService ▶[ƒ CrudService()]

And so, we see that Reflect.getMetadata(‘design:paramtypes’, target)returns a list of dependencies of the class being decorated. Where this information comes from, it is better to read in the original source.

To simplify, TypeScript will automatically add the metadata to Reflect (store/object).

Let's try to decorate the class method

export const Validate = () => {
return (target, key, descriptor) => {
console.log(
'[Validate]',
target,
Reflect.getMetadata('design:paramtypes', target, key)
// Here we need to pass the decorated property name (key = getData) to get relevant metadata from Reflect.object
);
console.log(
'[Validate] returntype',
target,
Reflect.getMetadata('design:returntype', target, key)
);
};
};
@Injectable()
class CrudService {
@Validate()
getData(entity: string): string{
return `Some Data from -> ${entity}`;
}
}
// LOG: [Validate] paramtypes [ƒ String()]
// LOG: [Validate] returntype ƒ String()

In other words, Reflect is an object that stores some data. What data? The metadata. TypeScript in this object adds information about the types in the signatures of the class and methods and the types that these methods return.

Knowing all this and having access to the Reflect object, we can write the following function:

export const resolve = (target: Type<any>) => {
// Get list of dependent classes
let tokens = Reflect.getMetadata('design:paramtypes', target) || [];
// Create dependent instances recursivly
let injections = tokens.map((token) => resolve(token));
// Return class instance with dependets instances.
return new target(...injections);
};
const testMoviesR: MoviesService = resolve(MoviesService);
console.log(testMoviesR.getComments());
// LOG: Some Data from -> /comments
console.log(testMoviesR.getMovies());
// LOG: Some Data from -> /movies

All dependencies will be automatically resolved.

However, this is still not ideal — for example, CrudService it will be created twice. But at the beginning of the article, we talked about singleton.

To begin with, for a more straightforward implementation, instead of the resolve function, we will create an Injector and add the necessary functionality to it.

export const Injector = new (class {
instMap: Record<string, any> = {};
resolve<T>(target: Type<any>): T {
let tokens = Reflect.getMetadata('design:paramtypes', target) || [];
let injections = tokens.map((token) => {
return Injector.resolve<any>(token);
});
if (this.instMap.hasOwnProperty(target.name)) {
return this.instMap[target.name];
} else {
this.instMap[target.name] = new target(...injections);
return this.instMap[target.name];
}
}
})();
const testMovies = Injector.resolve<MoviesService>(MoviesService);

This may not be the most elegant solution, but it will work as an example :)))

Summing up:

I hope I managed to talk about the following points in this article:

Reflect on a class is a kind of object into which TypeScript adds information about the types in the constructor, the types of method signatures, and the types that these methods return when compiling.

With the help of the reflect-metadata library, we can get this information and use it for our purposes.

In the following article, we will analyze how to add the information we need to the Reflect object and how we can use it.

If you like this article, comment and react ♾️ times.

Follow me on Twitter, Medium Dev/To for blog updates.

Check out my website and Youtube for videos and public talks.

Feel free to ask a question.

Thanks a lot for reading!

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Implement features, document your code, or refactor your projects.
Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay