Aurelia DI with typescript interfaces
Asked Answered
V

5

18

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)

Virulence answered 16/6, 2015 at 16:5 Comment(1)
Comments from the gitter channel: But on a related note, it can’t work with interfaces because TypeScript compiles those away at runtime.Cobblestone
L
8

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

Lowering answered 16/6, 2015 at 19:32 Comment(2)
Thanks for getting back to me. Good to know about the overhaul - any idea when that'd be (roughly)? I should have known about the TS interfaces being compiled away - makes sense - but the idea stands. di-ts, for example, uses classes rather than interfaces, presumably as a bit of a fudge.Virulence
yeah - exactly. in this example (github.com/VaclavObornik/di-ts#maints), the classes are used as keys in the injector container. MockEngine lets the container know that it can be used in place of Engine, and then MockEngine is registered with the container.Lowering
F
3

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.

Fadden answered 24/9, 2016 at 0:44 Comment(11)
I just tried this trick but it doesn't work. I'm using tsc v2.0.3Longboat
Does the compiler emit any errors? It's working fine for me on 2.0.0-2.0.3 in an electron app. It may be possible that other JS engines treat the empty object as the same reference.Fadden
All I can say is that it's still working for me using TypeScript 2.1 with the latest version of Aurelia's container.Fadden
I believe 2.1 is in beta, but OKLongboat
This just doesn't work with Typescript 2.1.4 using constructor injection. I just get an instance of the empty object from const IFoo = {}Anglia
Tim, it is still working for me using TypeScript 2.1.5 (and it did on 2.1.4). I'm using it in a production application. Are you using @autoinject or manually specifying the interfaces? I use @autoinject everywhere.Fadden
My apologies... this works in all cases except @autoinject. I'll see if I can fix that and submit a PR. You'll have to use @inject(IFoo, ...) for now.Fadden
Would const IFoo = Symbol() works the same or better?Datha
@Datha Symbols work, and I'm assuming they're more efficient than an empty object. Good call.Fadden
Found a way to make it work with @autoinject: stackoverflow.com/a/39671387Fluviatile
I could not get this to work as documented. Alon's decorator below did not help initially either. However const IFoo = Symbol.for("whatever") got it working for the @inject decorator and then Alon's @i decorator allowed the @autoinect decorator to work. I'm unsure why the symbol needed to be global in my instance. I am using webpack,,,,Volition
R
3

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
Rainer answered 20/10, 2016 at 20:32 Comment(2)
true.. but always notice that the compiled JavaScript code will vary from interface usage.. ..i.e. interface IFoo {} class Foo implements IFoo {}Antipathy
true.. but always notice that the compiled JavaScript code will vary from interface usage.. ..i.e. interface IFoo {} class Foo implements IFoo {} compiles into class Foo {} and..... export abstract class IBoo {} class Boo implements IBoo {} compiles into... define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); class IBoo { } exports.IBoo = IBoo; class Boo { } }); what gives you a different amount of code...Antipathy
F
0

Loved the idea of Frank Gambino and found a way to make it work both with @inject and @autoinject. The trick is to use a custom parameter decorator (since interface is reserved in TypeScript I called it @i).

The decorator parts:

myClass.ts

import { autoinject } from 'aurelia-framework';    
import { i } from './i.ts';
import { IFoo } from "./ifoo.ts";    

@autoinject
export class MyClass {
    constructor(@i(IFoo) foo: IFoo) {
        foo.doSomething();
    }
}

i.ts:

import "reflect-metadata";

/**
 * Declare the interface type of a parameter.
 *
 * To understand more about how or why it works read here:
 * https://www.typescriptlang.org/docs/handbook/decorators.html#metadata
 */
export function i(interfaceSymbol: symbol) {
    return function (target: Object, parameterName: string | symbol, parameterIndex: number) {           
        var paramTypes = Reflect.getMetadata('design:paramtypes', target);
        paramTypes[parameterIndex] = interfaceSymbol;
        Reflect.defineMetadata('design:paramtypes', paramTypes, target);
    }
}

The rest of it is exactly like Frank Gambino answer but I added it for completeness ...

ifoo.ts:

export interface IFoo {
    doSomething(): void;
}

export const IFoo = Symbol("IFoo"); // naming the symbol isn't mandatory, but it's easier to debug if something goes wrong

some.ts:

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

export class Bar implements IFoo {
    doSomething(): void {
        console.log('it works!');
    }
}

main.ts:

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

...

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

...

And it can actually work with other DI containers. To make it work with Angular2 (although why would you? Aurelia is much more awesome :) you just need to change the type of interfaceSymbol in the i.ts file to any and instead of Symobl("IFoo") write new InjectionToken("IFoo") (the InjectionToken class is an Angular thingy and sadly enough they don't support Symbol as an injection token, at least for the time being).

Fluviatile answered 14/7, 2017 at 14:42 Comment(0)
S
0

I had a different approach to solving this that worked for me.

Take the following class:

export class Foo implements Bar {

}

I changed this to the following:

import { Container } from 'aurelia-framework';

class Foo implements Bar {
}

export var foo = Container.instance.get(Foo) as Bar;

Now I can just do the following to get a typed singleton instance of the class:

import { foo } from 'foo';
Schoolman answered 20/9, 2018 at 10:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.