Dependency Injection in Vue.js with good-injector-vue Vue.js-Plugins with TypeScript

Dependency Injection with TypeScript: good-injector

Published on Monday, February 12, 2018 6:00:00 AM UTC in Announcements & Programming & Tools

In classic languages and frameworks on the server side, like C# and ASP.NET (Core), dependency injection today feels like a natural piece of the infrastructure. With more and more complex applications being developed in the browser, developers seek similar mechanisms and features for client-side languages like TypeScript too. Not only does it reduce coupling and simplify testing; nobody wants to hand-craft deep dependency trees and manually tune these after each refactoring. In TypeScript, there are some conceptual differences to how the topic is typically approached in languages like C#. In this post I briefly talk theory and options, then proceed to create a simple IoC container for TypeScript from scratch: good-injector.

You can find the full source code on GitHub and the respective package on NPM.

The Theory

The major mental stumbling block you need to overcome is that in C#, dependency injection is very tightly coupled to interfaces. Yes, you can also use concrete types in most frameworks, but the general idea of inversion of control typically revolves around passing around interfaces without knowing about concrete implementation details. In TypeScript we also have interfaces, but the problem is that they have no runtime equivalent. This means that no JavaScript constructs are emitted by the TypeScript compiler for interfaces, which in turn leads to the problem that you simply cannot use interfaces to base a dependency injection mechanism on. This fundamental difference of TypeScript to languages like C# makes it necessary to embark on a quite different philosophy. The available options typically are:

  1. Use a combination of strings and interfaces.
  2. Use a combination of symbols and interfaces.
  3. Work with abstract classes instead of interfaces.

The first two options are quite similar: because interfaces alone do not work as required, you tie them to another piece of data to uniquely identify them. I dislike these options for the following reasons:

  • String-based solutions have no tooling or compiler support. They limit your options for refactorings, even for reference/usage searching and increase the likelihood of errors. If magic strings are automatically inferred from types, you may also run into issues with name mangling by minification tools.
  • These approaches in general are based on conventions. Due to the mentioned limitations of interfaces, the connection between i.e. a symbol and an interface is not enforced by tooling and solely relies on the developer doing the right thing. In all frameworks I know misconfiguration or misuse is easily possible and will only result in runtime errors (which sadly might be very subtle if inheritance or polymorphism is involved).

This leaves option 3, using abstract classes. For a lot of people, an uncomfortable feeling is connected with this, but it has some great benefits as we'll see in a moment. Depending on how you use these classes they really are no different to interfaces if you don't want that.

Available Frameworks

If you're only interested in the development details of "good-injector", you can skip this section.

I've been doing some research around the topic during the last months and kept an eye on various available options. One thing I realized is that a lot of the implementations are bound to a specific framework. For example, Angular has its own dependency injection mechanism, Aurelia has one, and there are various third-party implementations tailored to Vue.js also, of course. But I was looking for a more generic implementation, mostly because I wanted to provide DI features in common libraries that are not tied to a particular UI framework. Building bridges or adapters to those frameworks is another issue to solve - later.

If you do your searching for something like this, you'll very quickly bump into Inversify. It supports multiple features and scenarios, but if you read the samples you'll realize that they are exactly based on the combination of symbols and interfaces I've been talking about above. The involved ceremony, to me, is astounding. You have to decorate the classes, the constructor arguments, create your symbols, tie them to interfaces. Wow. In their architecture documentation, the authors of the framework talk about how it is heavily influenced by Ninject. Now, if you are a regular reader of my blog, you know I have some concerns with Ninject, and because its setup has become overly complex and obscure over the course of years, we've largely abandonded it and moved on to other frameworks in all our projects. Reading through the documentation on Inversify, I got a quite similar impression: the "annotation" phase transforms meta data into "requests" and "targets", the "planning" phase creates a "resolution context" so the "planner" can create a "plan" that is routed through optional "middlewares" during the equally named phase until the "resolver" does the actual work during the "resolution phase", only to put the results for finalization into the "activation phase" where they can potentially be replaced by "proxies" if you like. I'm sure there are very good reasons for all of this, probably because of the extremely versatile features and options available. For me though, this instantly brings back memories from the "rebind" and "when injected into" horrors I've gone through with it's role model, Ninject itself. All these features sound good in theory, but quickly become a maintenance problem and, in my humble opinion, only add more possibilities to shoot yourself in the foot.

Using this project hence was not an option for me. Other alternatives I found are not worth mentioning[1][1]
Did I miss an important one? Feel free to recommend your favorite to me!
. What to do? At some point, I asked myself: how hard can it be to write one yourself? I have written IoC containers in the past, for example as part of my Windows Phone framework "Liphofra". The only limitation being that I've never done it in TypeScript. Well, let's try!

A Proof of Concept: good-injector

A minimum implementation would offer features to register type mappings (from abstract type to the implementation to use) and of course to resolve those registered types, resolving all dependencies recursively on the way. That doesn't sound too hard, right? But before we start, let's once again lay out the ground rules for the implementation:

  • Must be highly opinionated and only support abstract or concrete types mapped to their implementations (including themselves).
  • Must be type-safe with a good amount of compiler support (no magic strings, no convention based approach).
  • Must be strict and explicit, meaning no silent fails or unexpected outcome for misconfigurations, no intransparent black magic.

OK, go!

Requirements

In TypeScript, whenever you want to work with meta data of code, meaning type information and similar things, you inevitably need to use two features: decorators and emitting meta data, for both of which you need to enable explicit compiler support. I won't go into the details here because it's simply a huge topic and out of scope for this article, but I highly recommend reading the linked explanations on the TypeScript web site to understand the details.

In the tsconfig.json, these features translate to the following settings:

"experimentalDecorators": true,
"emitDecoratorMetadata": true 

Don't worry about the "experimental" part. The TypeScript team is extremely careful about not including anything in the language that doesn't have a good chance of being adopted into e.g. the official ECMAScript standard at a later point. These two options have been around for a while, and are used by all frameworks and libraries that want to provide reflection-like features (including the ones mentioned in this article).

Defining some base types

These are the base building blocks we are going to work with. First of all, let's define some formal declaration we can use for abstract classes as well as concrete implementations. The latter one is easy and can be found around in the documentation of TypeScript also: it's something that can be "newed up". For abstract types, the definition is a bit different. Because these types cannot be created, the definition can be restricted only to something like: it's a function with a prototype.

export type Constructor<T> = Function & { prototype: T }; // this describes an abstract class constructor
export interface IConcreteConstructor<T> { new(...args: any[]): T; }

That's what we need for registrations: we map from abstract types to concrete types. Please note that the former is a more general definition of the latter, meaning with this definition you will be able to map concrete types to themselves or derived types also.

Now, as I mentioned in the requirements section, we need to work with at least one decorator that is used by consumers of our implementation, so we have some meta data to work with. Naturally, a DI container would want to support constructor injection, so the decorator we create needs to emit meta data on the constructor. It's a bit irritating first that these decorators are in fact not attached to the constructor itself, but to the class definition. You'll get used to that, and probably have seen this in other frameworks like Angular or Aurelia already. Our decorator can be empty, because the emitDecoratorMetadata flag of the TypeScript compiler does all the work for us behind the scenes: once attached to a class, our decorator lets it emit meta data about the type and its constructor automatically.

export function SupportsInjection<T extends { new(...args: any[]): {} }>(constructor: T) {
    // tslint:disable-next-line:no-empty => decorator has no content but still does its magic
}

To use that meta data later on, we need to add the "reflect-metadata" package to our project and import it properly in our code files:

import "reflect-metadata";

Another piece of the puzzle are the types of registration we want to support. There are typically at least to types we need all the time: transient and singleton registrations. The first one means that whoever asks for a dependency will receive a new instance of the resolved type every time, whereas the second one means an instance is created once and then returned every time it is required. Both scenarios are very valid and required in most applications.

I could have used something like an enumeration to distinguish between those to, but like I said I've written one or the other container in the past, and in the back of my head I already have some additional features that might become interesting in the future (see below). This is why I decided against using a simple enumeration, but rather make the registration calls explicit (i.e. the user has to call something like registerTransient or registerSingleton later, see below). In any case, what we need is different registration implementations to handle this requirement. Please note that I'm not exporting any of these, they are solely for our internal mechanics, the user never will have to deal with these later on:

// tslint:disable-next-line:no-empty-interface => marker
interface IRegistration {    
}

interface ITypedRegistration<T> extends IRegistration {
    resolve(argumentBuilder: (type: IConcreteConstructor<T>) => any[]): T;
}

class TransientRegistration<T> implements ITypedRegistration<T> {
    constructor(private _type: IConcreteConstructor<T>) {
    }
    
    // implement rest later
}

class SingletonRegistration<T> implements ITypedRegistration<T> {
    constructor(private _type: IConcreteConstructor<T>) {
    }
    
    // implement rest later
}

Ignore the details for now, I'll come back to all of these later. I just wanted to introduce the types early because they'll show up in a minute when we start with the actual container details. That's it, let's dive into the actual implementation.

Implementing the registration

Whenever someone registers a mapping with the container, we capture the meta data for the target type that describes the types of the constructor arguments. Alternatively, I could have done that every time when someone wants to resolve a type later, but it seems cleaner to only perform this once, and it opens up the door for further validation and consistency checks later on-and these typically should happen during registration, not at an arbitrary later point in time when someone wants to resolve a type in a completely unrelated location of the application. Here's the registration code of the container:

private _parameterTypes: Map<Function, any[]> = new Map<Function, any[]>();
private _providers: Map<Function, IRegistration> = new Map<Function, IRegistration>();

public registerTransient<From, To extends From>(from: Constructor<From>, to: IConcreteConstructor<To>): void {
    this.register(from, to, new TransientRegistration<To>(to));
}

public registerSingleton<From, To extends From>(from: Constructor<From>, to: IConcreteConstructor<To>): void {
    this.register(from, to, new SingletonRegistration<To>(to));
}

private register<From, To extends From>(from: Constructor<From>, to: IConcreteConstructor<To>, registration: IRegistration): void {
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", to);
    this._parameterTypes.set(to, paramTypes);
    this._providers.set(from, registration);
}

That looks surprisingly simple:

  • Someone calls the respective register method that internally creates the right registration instance
  • A common, private register method gets the meta data and puts it into a map, and does the same with created the registration instance

Please note that I put a constraint on the To generic parameter. It needs to extend the From type. This will make sure that you can only register compatible types later. The compiler won't build any code where the source type is not interchangeable with the target type, another benefit of this type-safe solution.

Now, before we continue, let me introduce a small improvement to this. In the current implementation, when a user wants to register an already concrete type to be resolved to itself (so the dependency chain is resolved automatically), they would have to write something like this:

container.registerSingleton(MyType, MyType);

We can make this scenario simpler by introducing explicit overloads for this use case in the container. Like this:

public registerSingleton<T>(self: IConcreteConstructor<T>): void;
public registerSingleton<From, To extends From>(when: Constructor<From>, then: IConcreteConstructor<To>): void;
public registerSingleton<From, To extends From>(when: Constructor<From> | IConcreteConstructor<From>, then?: IConcreteConstructor<To>): void {
    if (then == undefined) {
        // the reason we can safely do this type case here is that there are only two overloads;
        // the one overload that has no second argument (no "to") ensures that the first one is IConcreteConstructor<T>
        // also: From extends From === true
        then = when as IConcreteConstructor<To>;
    }

    this.register(when, then, new SingletonRegistration<To>(then));
}

Note the comment that explains why it's safe to do the type cast here.

With these overloads, for binding types to themselves the user can now simply write:

container.registerSingleton(MyType);

That's it for the registration. Now on to resolving types!

Implementing the resolution

The resolution works like this:

  1. Get the registration for the requested type that has been registered before
  2. Delegate the actual resolution to the registration

Let's take a look:

public resolve<T>(from: Constructor<T>): T {
    const registration = this._providers.get(from) as ITypedRegistration<T>;
    if (registration == undefined) {
        throw new Error(`No registration found for type '${from.name}'`);
    }

    return registration.resolve((type) => this.createArgs(type));
}

As you can see, we can determine whether a suitable registration is available in a type-safe manner here, and provide a meaningful error otherwise. The compiler helps us (and our users) to do the right thing.

The only "trick" here is that last line of code, where the mechanics for the type resolution and the involved recursive logic to build the dependency tree are passed to the registration as a function delegate, so this logic can stay in the container itself (where the parameter types are stored anyway) and doesn't need to be potentially duplicated in every registration. That logic looks like this:

private createArgs<T>(type: IConcreteConstructor<T>): any[] {
    const paramTypes = this._parameterTypes.get(type);
    if (paramTypes == undefined) {
        return [];
    }

    return paramTypes.map((x) => this.resolve(x));
}

We simply determine the meta data for the parameters of the passed in type, and resolve each parameter recursively. There are two things to note here:

  1. The check for undefined is due to a limitation of the reflect API. I talk about this a bit more in the final section of this article.
  2. The recursion does not check for circular dependencies, meaning that the app will crash if this happens. It's a proof of concept at this point, yes, but I would be reluctant to fix this even in a final product: having that situation is plain wrong (the container cannot solve the given problem), and if there's really a legitimate situation where it's OK to have something like that (is there?) I would rather force you to build your dependency tree by hand.

With that, the container implementation is complete. Wait, what? Yes, it really only is like 50 lines of code, including extensive comments, empty lines and curly braces. The last missing piece now is the resolution logic of the individual registration implementations.

Registration implementations

These are very straight-forward too. Let's look at the transient registration first, the one that provides a new instance of the requested type every time:

public resolve(argumentBuilder: (type: IConcreteConstructor<T>) => any[]): T {
    const args = argumentBuilder(this._type);
    return new this._type(...args);
}

Uhmm... get the arguments, new-up an instance. Right.

The singleton implementation is slightly more sophisticated:

private _instance: T | undefined;

public resolve(argumentBuilder: (type: IConcreteConstructor<T>) => any[]): T {
    if (this._instance != undefined) {
        return this._instance;
    }

    const args = argumentBuilder(this._type);
    this._instance = new this._type(...args);
    return this._instance;
}

The logic here is to check first if we created an instance before, and if we have, return that. The remaining logic is similar to the transient one: new-up a new instance and return it-but save it first so it can be returned on subsequent calls. Backend developers that have dealt with singletons in the past may note there's no locking involved here. That's OK because of the single-threaded nature of JavaScript.

Well, that's it. All in all the transpiled full implementation, including the decorator and all, is less than 80 lines of code. But... does it really work?

Testing

To prove everything's working, I added a bunch of unit tests. The scopes (different kinds of registrations) in particular are worth testing of course. So one of the scenarios looks like this. First some test types to work with:

// in Parent.ts
import { SupportsInjection } from "../../../src/Index";
import { Child } from "./Child";

@SupportsInjection
export class Parent {    
    public constructor(public child: Child) {        
    }
}

// in Child.ts
export class Child {    
}

See the usage of the decorator? That makes sure the information on the constructor argument of type Child is emitted as meta data for the Parent implementation. Using these types in an application now could look like the following (unit test) code:

@Test("when registered as transient should return new instances every time")
public scopeTest1() {      
    let container = new Container();        
    container.registerTransient(Child);
    
    let child1 = container.resolve(Child);
    let child2 = container.resolve(Child);
    
    Expect(child1).not.toEqual(child2);
}

As you can see, you can register the Child type for itself. That's why we build the overloads above. The benefit of doing something like this is that the container builds the dependency graph of constructor arguments for you. Meaning for example if you change the arguments later on, you won't have to fix potentially many places in your code, it simply will continue to work without manual changes.

Now a similar test for the singleton registration:

@Test("when registered as singleton should return the same instance every time")
public scopeTest2() {      
    let container = new Container();        
    container.registerSingleton(Child);
    
    let child1 = container.resolve(Child);
    let child2 = container.resolve(Child);
    
    Expect(child1).toEqual(child2);
}

Nice! But of course the more interesting situations are when you're actually working with dependency trees. Like using the Parent type:

@Test("resolving transient parent with singleton child gets same child instance every time")
public scopeTest4() {      
    let container = new Container();        
    container.registerTransient(Parent);
    container.registerSingleton(Child);
    
    let parent1 = container.resolve(Parent);
    let parent2 = container.resolve(Parent);
            
    Expect(parent1).not.toEqual(parent2);
    Expect(parent1.child).toBeDefined();
    Expect(parent1.child).toEqual(parent2.child);
}

What does thist test tell you?

  • Parent instances could be resolved without errors.
  • Parent instances have received Child instances in their constructors.
  • The resolved Parent instances are not equal (transient registration works).
  • But their Child instances are equal (singleton registration works for injected dependencies).

For more tests, please take a look at the sources on GitHub.

Real-world sample

A more real-life scenario where you actually bind abstract types to implementations could look like this: let's say you want to build a logger implementation that performs all sorts of message formatting, provides details like log levels and so on, that you can use in your own, common libraries. The problem you're facing typically is that later on, when your library is used in applications, those applications will use whatever logging features comes with the UI framework of choice. But making use of these UI framework features in your general logger library or your other common libraries is bad design: you tie your common library to a particular UI framework and force all consumers of your library into using that framework too. Things get worse when you want to support multiple frameworks. What do you do? Bundle multiple of those as dependencies in your common library?

With a DI container like this, here's what you would do:

  • Implement your (abstract) logger with all the common features that are shared across potentially multiple applications. No dependencies on a particular UI framework here.
  • Implement one or more adapters for particular UI frameworks. These would derive from your abstract logger and add all specific code to connect to the logging features of that target framework. Here it's OK to have the framework dependency, because everybody using that adapter consciously decided to use the target framework anyway.
  • Package your common implementation and the adapters separately, so you can cleanly work with the dependency-free parts in all your other common libraries.
  • Register the concrete adapter as type to resolve for your abstract logger on the application level.

With this setup, you are free to have multiple, clean adapters for different target frameworks, and, even better, you can use your abstract logger in all your other common libraries without tainting them. At runtime, they will be provided with a specific implementation that suits the current application environment, but at design time you are free of all of these. Here's a sample:

export abstract class Logger {
    public logInfo(message: string): void {
        this.writeMessage("Info: " + message);
    }

    public logError(message: string): void {
        this.writeMessage("Error: " + message);
    }

    protected abstract writeMessage(message: string): void;
}

Now isn't that a feature-rich, well thought-out general logger API. You would use that everywhere in your other common libraries, and even in your application code if you like.

@SupportsInjection
export class MyOtherCode {
    public constructor(logger: Logger) {
        logger.logInfo("I've been constructed, yay!");
    }
}

Then, provide a concrete implementation for the target framework of choice. Here, I use the "console" framework :).

import { Logger } from "./Logger";

export class ConsoleLogger extends Logger {
    protected writeMessage(message: string): void {
        // tslint:disable-next-line:no-console
        console.log(message);
    }
}

In your application, set up the registration accordingly, so everybody receives a ConsoleLogger when the ask for an injection of Logger...

let container = new Container();
container.registerTransient(Logger, ConsoleLogger);

... and at runtime your log messages are put into the console, from all your application and common library code. Nice?

This sample also can be found in the GitHub repo as unit tests.

Outlook

As I wrote above: this project already is "a thing".

You can find the full source code on GitHub and the respective package on NPM.

But, if I push on with this project, what are the improvements I consider for the future?

  • Create an adapter for Vue.js.
  • Make the decorator register which types have been decorated. At the moment, with "reflect-metadata" you can't distinguish between "has been decorated and emitting metadata was not required" and "has not been decorated". This means that it's not possible to test whether someone has forgotton to decorate or if the correctly decorated type has no constructor arguments. This can be solved by registering decorated types by the decorator itself, and then tighten up the resolve implementation with appropriate validation.
  • Adding more registration scopes, in particular for existing instances and factories. Both are valid use cases, to add things to the container you received from elsewhere, or to resolve types based on criteria that is out of scope for the container.
  • Passing through arguments during resolution. This is a use case that came up a lot in the past, i.e. you want to resolve the dependencies of a type but there's one or more additional dynamic arguments that you need to pass on to the resolved type. A pattern to work around this is to use factories that set properties or call initialization methods on the resolved type. But it may be nice to have something like this built-in.

Things I won't consider:

  • Adding support for convention-based or string-based features (no symbols + interfaces, no magic strings)
  • Adding support for plain JavaScript, unless the required features are supported in the future

What do you think?

Tags: Dependency Injection · Good-injector · TypeScript