TypeScript Decorators: Dependency Injection

TypeScript

In this article, we’ll take a look under the hood, and learn how the TypeScript compiler transforms decorators to a native JS code. We’ll focus only on class decorators and see how we can use generated metadata to implement dependency injection.

Decorators

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true
}
}

TypeScript includes experimental support for emitting certain types of metadata for declarations that have decorators. To enable this experimental support, you must set the emitDecoratorMetadata compiler option either on the command line or in your tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

The native JavaScript support for a metadata reflection API is in development. There is a proposal to add Decorators to ECMAScript, along with a prototype for Reflection API for Decorator Metadata.

Some of the guys from the TypeScript team have developed a Polyfill for the prototype of the Reflection API and the TypeScript compiler is now able to emit some serialized design-time type metadata for decorators. We can use this metadata reflection API polyfill by using the reflect-metadata package:

npm install reflect-metadata

We also need to import reflect-metadata in our index.ts:

import 'reflect-metadata';

For the moment there are available only three reflect metadata design keys:

  • Type metadata uses the metadata key design:type.
  • Parameter type metadata uses the metadata key design:paramtypes.
  • Return type metadata uses the metadata key design:returntype.


Dependency Injection

In case you have no idea what DI is, I highly recommend to get in touch with it. Since this post should not be about the What?. but more about the How? let’s try to keep this as simple possible at this point:

Dependency injection is a technique whereby one object supplies the dependencies of another object.

What does that mean? Instead of manually constructing your objects some piece (often called Injector) of your software is responsible for constructing objects.Imagine the following code:

To get an instance of Example you’d need to construct it the following way:

const example = new Example(new Service1(), new Service2(new Service1()));

By using an Injector, which is responsible for creating objects, you can simply do something like:

const example = Injector.resolve<Example>(Example);

We will implement our very own (and very basic) Injector. In case you’re just looking for some existing solution to get DI in your project you should take a look at InversifyJS, a pretty neat IoC container for TypeScript.

What we’re going to do is we’ll implement our very own Injector, which is able to resolve instances by injecting all necessary dependencies. But how we can know about dependencies of a class at runtime? Well, here comes the reflect-metadata package. We can read metadata about class constructor by:

Reflect.getMetadata('design:paramtypes', Example);

It will return undefined, because TypeScript will not emit metadata about class if it has not been decorated. As in this PR it explicitly was designed to emit metadata just if there is a decorator on a class. So we need a decorator in order our class do emit metadata. Let’s implement:

Let’s have a look at interface Type<T>: It’s generic type for constructors. Class decorators receive as target argument constructor of class. The Injectable decorator does nothing but logs metadata design:paramtypes of class constructor it’s applied to.

Back to our example: after applying Injectable decorator to Example class TypeScript will emit metadata about that class:

So now if we call

Reflect.getMetadata('design:paramtypes', Example); // [ [Function: Service1], [Function: Service2] ]

it will return Array of types of dependencies. How this works? Let’s see the generated JavaScript code:

On line 28 we can see that at runtime it adds design:paramtypes metadata to Example with value of [Service1, Service2]. It stores this metadata in WeakMap with key of target and value of array. So that we can know which parameters receives Example class constructor.

Now lets implement our Injector which will find out and resolve dependencies of any class.

We have a DI container where will be stored instances of dependencies. Our resolve method will receive class constructor as parameter, read dependencies’ types and recursively resolve all dependencies. Let’s get back to our (now slightly extended) example at the beginning and resolve it via the Injector:

Console output:

[ [Function: Service1] ] [ [Function: Service1], [Function: Service2] ] Service1 Service2

Wohoo!. Our Injector successfully injected all dependencies. Basically this is how an injector could work. There are still many things to do:

  • error handling
  • handle circular dependencies
  • ability to inject more than constructor tokens
  • etc.

The entire code (including examples and tests) can be found on GitHub.

BIO

Sasun Stepanyan is a Senior Software Engineer at ServiceTitan. He’s been with ServiceTitan for a year and began his career in engineering three years ago, quickly demonstrating strong talent in the field and becoming a senior engineer. He loves creative challenges – when he’s not coding, you might find him playing piano.