Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript interfaces for Dependency Injection #3060

Closed
pavelsavara opened this issue May 6, 2015 · 15 comments
Closed

TypeScript interfaces for Dependency Injection #3060

pavelsavara opened this issue May 6, 2015 · 15 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@pavelsavara
Copy link

I tried to imagine how could I use new Decorator Metadata in order to autowire dependency injection with TS interfaces. Test with tsc 1.5 beta gives me Object constructor instead of interface constructor (perhaps obviously), but that limits the ability of DI container to wire the dependency by type of the interface (as is usual in C#).

This would be the sample.

///<reference path="node_modules\reflect-metadata\reflect-metadata.d.ts"/>

import "reflect-metadata";

export interface IEngine{
}

export class Engine implements IEngine {
}

export class TestEngine implements IEngine {
}

function Inject() {
  return function (target: Function) {
  }
}

@Inject()
export class Car {
  constructor(public engine: IEngine) {}
}

tcs 1.5beta compiles to this

var Car = (function () {
    function Car(engine) {
        this.engine = engine;
    }
    Car = __decorate([
        Inject(), 
        __metadata('design:paramtypes', [Object])
    ], Car);
    return Car;
})();

Ideally the line should be

        __metadata('design:paramtypes', [IEngine])

and IEngine could be dummy class/constructor.

Or it could be string full name of the interface type

        __metadata('design:paramtypes', 'IEngine')

I tried to explain this to Angular team before here
angular/angular#135

@mhegazy
Copy link
Contributor

mhegazy commented May 6, 2015

and what is IEngine? given your sample IEngine === undefined...

@pavelsavara
Copy link
Author

That's right, ts does't generate dummy classes for interfaces, (yet ?).
But it would be ideal to generate something visible on runtime.
It doesn't have to be class, it could be string with interface name.
So that DI framework could bind specific implementation of the interface to it, in it's configuration.

@mhegazy
Copy link
Contributor

mhegazy commented May 7, 2015

I would say what you want is #3015 instead :D. this way you get the type information at runtime.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label May 7, 2015
@pavelsavara
Copy link
Author

Not really, my point is about interfaces. If the class member in #3015 was defined as interface, what would get generated for runtime ?

@mhegazy
Copy link
Contributor

mhegazy commented May 7, 2015

The structure of the type. See @rbuckton's comment: #3015 (comment)

@pavelsavara
Copy link
Author

I see, if full type information would be available on runtime as @rbuckton proposes, including interfaces, that would solve this need greatly. I really like that proposal.

But this specific scenario could be solved with less than that.

@sccolbert
Copy link

FWIW - I've built a simple DI library for typescript which (ab)uses the fact that there is a separation between the type namespace and the variable namespace.

It uses a runtime token which captures the interface type, but which is assigned (by convention) to the same name as the interface it is representing. This allows the dependencies to be declared, registered, and resolved at runtime using the token object, and it's all completely type-safe.

@pavelsavara
Copy link
Author

@sccolbert nice pragmatic workaround.
In this request I hope to get compiler to provide the type 'handle/token' for the interface injection in consuming component. (instead of manual $inject)

@sccolbert
Copy link

@pavelsavara agreed, that would be ideal.

@brendanowen
Copy link

Currently I have to manually give my interfaces a name. By convention I exploit you can use the same name of the string variable as the interface name. TypeScript works out the difference depending on the context. For example someNameSpace.ISomeInterface is a string normally and is an interface if used with a : I still have to manually match the order to the $inject array with the parameter list in the constructor. I would love to be able to remove the need to manually give the interface a string name and line up the string names with the interface names. I am considering writing my own grunt task to preprocess my TypeScript files to annotate them with the convention I use below. I would like to say how this above issue plays out first.

// File: some/name/space/ISomeInterface.ts
module someNameSpace {

export var ISomeInterface:string = "someNameSpace.ISomeInterface";
export interface ISomeInterface {

    someProperty:number;
}

}

// File: some/name/space/SomeConcrete.ts
///
module someNameSpace {

export class SomeConcrete implements someNameSpace.ISomeInterface {

    public someProperty:number = 10;
}

}

// File: some/name/space/SomeObject.ts
///
module someNameSpace {

export class SomeObject {

    public static $inject:string[] = [someNameSpace.ISomeInterface];

    constructor(injectedObject:someNameSpace.ISomeInterface) {
    }
}

}

// File: some/name/space/application.ts
///
///

someModule = angular.module("someModule", []);
someModule.service(someNameSpace.ISomeInterface, someNameSpace.SomeConcrete);
someModule.service("someNameSpace.SomeObject", someNameSpace.SomeObject);

@rbuckton
Copy link
Member

rbuckton commented Jun 8, 2015

There are two problems with using TypeScript interfaces for any kind of dependency injection:

  • TypeScript interfaces use structural typing. In other words, the following two interfaces are synonymous:
interface A { x: number; }
interface B { x: number; }
  • Interface names are unreliable. In the face of import aliases in ES6, and our support for separate compilation for modules, it may not be feasible to accurately map an interface usage in one file back to its declaration in another module by name alone.
  • Classes at run time do not carry information about their implemented interfaces.

As such, dependency injection scenarios using decorators will always need some other means for identifying dependencies. This either means using classes (as they have a unique runtime identity), strings, symbols, or some other unique key.

@pavelsavara, there are issues with emitting any kind of type handle for interfaces:

  • These handles would violate structural typing
  • There would need to be a way to uniquely define them in a reliable fashion not only from build to build but also through declaration files.
  • There would need to be a consistent approach to applying them to classes.

The comment that @mhegazy mentioned points to a gist that describes a JSON-like language for describing types. This is more useful for scenarios like runtime type assertions than it is for dependency injection, and would necessitate an additional library just to be able to easily reason over things like union types, etc.

As it stands, the approaches that @sccolbert and @brendanowen outline above are a consistent and convention based compromise that looks viable. That said, we will continue to investigate suggestions and alternatives.

@pavelsavara
Copy link
Author

@rbuckton thanks for your research. Few ideas

  • if you emit metadata about each compile-time interface, you could let the DI library to figure out the structural equality on run-time.
  • the DI could be specifically configured by the user that the A == B. Even better, if the metadata itself would implement equals() method.
  • perhaps you could emit the metadata only from .ts files, but not from .d.ts
  • perhaps interface itself could be annotated by desired global run-time handle or global-name ?
  • could we 'export interface' to overcome module boundaries ?

@sccolbert Ideas in different direction

  • what if constructor/method parameters could be annotated individually (like in C#), would it allow us to improve on manual $inject ?
  • what if typescript compiler would allow some meta-programming/macros ?

@mhegazy
Copy link
Contributor

mhegazy commented Jun 13, 2015

looks like this is already covered in #3015 (comment)

@mhegazy mhegazy closed this as completed Jun 13, 2015
@thomas-darling
Copy link

@mhegazy Sorry to revive this discussion, but I don't understand how the referenced comment relates to the original issue, and I'm facing the exact same problem now.

I understand that TypeScript uses a structural type system, but I believe what we need here is not runtime type info about the interface, but an actual runtime reference to the interface type, such that it can be used as a key when resolving the dependency from a dependency injection container. If we go back to the code example at the top, I believe it illustrates the use case quite nicely:

We have an IEngine interface, implemented by both Engine and TestEngine.

We may then have a constructor somewhere that accepts an IEngine like this:

@Inject()
export class Car {
    constructor(public engine: IEngine) {
    }
}

During app startup we would have registered the preferred implementation:

dependencyInjectionContainer.register(IEngine, Engine);

This means the constructor will actually receive an instance of Engine while the IDE only sees it as an IEngine. This has two benefits: It allows us to swap out the implementation as needed, e.g. for testing, and it allows us to be explicit about which behavior we actually want to use - in this case, only the behavior exposed by IEngine, and not e.g. the behavior defined in a hypothetical IEngineServiceApi which is also implemented by Engine but only used when diagnosing engine problems.

However, this will only work if IEngine actually represents something that can be used as a unique key - it could just be an empty constructor function, just like an abstract class.
Currently, the interface is compiled away completely, which means we have no such unique key, and thus cannot make this use case work.

All we need is to have interfaces represented in the same way as abstract classes, as an empty constructor function:

var IEngine = (function () {
    function IEngine() {
    }
    return IEngine;
})();

This will allow us to use the interface type as a key, and thus enable this very desirable use case.
Is there any chance you could reconsider this change?

@mhegazy
Copy link
Contributor

mhegazy commented Dec 9, 2015

@thomas-darling please see the discussion in #3628

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants