Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aurelia DI with typescript interfaces

I've gone through the documentation of Aurelia DI and looked at the source code and wanted to share what I'm trying to achieve so that I can be shot down if I'm missing something obvious. I've looked at the samples here for TS with Aurelia but I can't see how it will work, and the docs are lacking.

What I want is:

dataProvider.js (the data provider interface)

export interface DataProvider {
  getData(): number;
}

itemDisplayer1.js (a class that will consume an injected class that implements the interface)

import {inject} from 'aurelia-framework';
import {DataProvider} from './dataProvider';

@inject(DataProvider)
export class itemDisplayer1 {
  constructor(public dataProvider: DataProvider) {
    this.dataProvider = dataProvider;
    this.data = dataProvider.getData();
  }
}

itemDisplayer2.js (another class that will consume an injected class that implements the interface)

import {inject} from 'aurelia-framework';
import {DataProvider} from './dataProvider';

@inject(DataProvider)
export class itemDisplayer2 {
  constructor(public dataProvider: DataProvider) {
    this.dataProvider = dataProvider;
    this.data = dataProvider.getData();
  }
}

GoodDataProvider.js

import {DataProvider} from "./dataProvider";

export class GoodDataProvider implements DataProvider {
  data = 1;
  getData() {
    return this.data;
  }
}

BetterDataProvider.js

import {DataProvider} from "./dataProvider";

export class BetterDataProvider implements DataProvider {
  data = 2;
  getData() {
    return this.data;
  }
}

And then somewhere(?) I would like to configure that itemDisplayer1 should be provided with an instance of GoodDataProvider and itemDisplayer2 should be provided with an instance of BetterDataProvider (1).

Then comes the problem of DI context. I'm not sure how to use container.createChild(). There's not much info on it that I can find. It creates a child container and it will delegate back to the parent when needed, but if I create 2 child containers and register one of the 2 providers with each child container, how would the itemDisplayer classes know which one to use (without changing their definitions and injecting in the parent container etc)?

Note: The lifetime management information doesn't live in the consumers or the providers of the dependencies (this is often done in the Aurelia DI examples and seems a little manufactured). I would expect this to be able to be defined when the consumers and providers are associated - point '(1)' above.

In summary, is this possible? Is this something that is on the cards for the near-ish future? Should I be trying to replace Aurelia DI with a custom container that meets my needs?

(The reason I'm trying to do this is that in order to evaluate js frameworks, the frameworks need to demonstrate a mature DI system with lifetime management/AOP etc capabilities as one of the criteria)

like image 296
John Stephenson Avatar asked Jun 16 '15 16:06

John Stephenson


3 Answers

from @eisenbergeffect: The DI is going to get some internal overhaul once we get the benchmarks written.

But on a related note, it can’t work with interfaces because TypeScript compiles those away at runtime.

You would have to come up with unique keys when you register your different types in the DI container and then specify the appropriate unique key in the @Inject(xxx) statement. The keys can be anything you like. Normally folks use the type itself for the unique key (this causes some confusion), but you could use strings, numbers, or anything else you like.

the unit tests are informative also: https://github.com/aurelia/dependency-injection/blob/master/test/container.spec.js

like image 84
Mike Graham Avatar answered Oct 25 '22 05:10

Mike Graham


So, as stated by others, TS compiles the interfaces aways and there is currently no way of doing this with pure interfaces. However, an interesting and often missed feature of TS is that it allows using class as an interface, this enables working around the current limitation.

export abstract class DataProvider {
  getData(): number;
}

@singleton(DataProvider) // register with an alternative key
export class MyAwesomeDataProvider implements DataProvider {
}

@autoinject
export class DataConsumer {
  constructor(dataProvider: DataProvider) {
  }
}

In the above code, we declare an abstract class DataProvider which will ensure that it's not compiled away by TS. We then register MyAwesomeDataProvider with an alternative key of DataProvider, which will return an instance of MyAwesomeDataProvider every time a DataProvider is requested.

As far as child containers go, you'd do container.createChild() which returns a new instance of the container and as long as the resolution is triggered from that child container, you should get the correct instance. The only problem is using decorators with two conflicting keys. Basically, the metadata lives on the class itself, so you can't have two instances registering under DataProvider, that would surely (tho I haven't tried it out myself) cause issues, the only way to go about it is use explicit registration. E.g.

export abstract class DataProvider {
  getData(): number;
}

export class MyAwesomeDataProvider implements DataProvider {
}

export class MyMoreAwesomeDataProvider implements DataProvider {
}        

child1 = container.createChild();
child1.registerSingleton(DataProvider, MyAwesomeDataProvider);

child2 = container.createChild();
child2.registerSingleton(DataProvider, MyMoreAwesomeDataProvider);

@autoinject
export class DataConsumer {
  constructor(dataProvider: DataProvider) {
  }
}

child1.get(DataConsumer); // injects MyAwesomeDataProvider
child2.get(DataConsumer); // injects MyMoreAwesomeDataProvider
like image 35
dryajov Avatar answered Oct 25 '22 06:10

dryajov


As Mike said, Aurelia doesn't support this dependency resolving feature yet. And interfaces get compiled away, so they cannot be used as keys (e.g. container.registerInstance(ISomething, new ConcreteSomething());

However, there is a small trick that can make it look like you're using the interface itself as the key.

foo.ts:

export interface IFoo {
  // interface
}

export const IFoo = Symbol();

bar.ts:

import {IFoo} from "./foo.ts";

export class Bar implements IFoo {
  // implementation
}

main.ts:

import {IFoo} from "./foo.ts";
import {Bar} from "./bar.ts";

...

container.registerInstance(IFoo, new Bar());

...

This compiles fine, and the compiler knows when to use the correct duplicate type based on the context in which it is used.

like image 24
Frank Gambino Avatar answered Oct 25 '22 07:10

Frank Gambino