microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.86k stars 12.36k forks source link

Discussion: (Reflective) Type Model #3628

Closed christyharagan closed 1 year ago

christyharagan commented 9 years ago

Hi all,

We currently have a AST model for TypeScript which is useful for compilers, editors, and linters.

However, it would be extremely useful to have a type model, a la Java's reflective model. There are a huge number of use-cases supporting this, but I will list a few here:

A good example of what could be achieved with this, is something like the Spring platform for Java where our applications are built as decorated components and most of the boilerplate code is abstracted away by the platform thanks to reflection and decorators.

I have a "first stab" at implementing such a thing: typescript-schema. It's not feature complete and the design isn't finished either. But hopefully it gives a feel for what a reflective model could look like.

Is this something the TypeScript team would be interested in having as a first class citizen?

danquirk commented 9 years ago

You might be interested in https://github.com/Microsoft/TypeScript/issues/2577. Reflection type functionality has been an oft requested but controversial issue for us.

mhegazy commented 9 years ago

Another related topic would be #3136.

I use injection decorators on class properties/constructor parameters, and use the type model to reflectively wire the correct instance.

can you elaborate on how this is done.

tobich commented 9 years ago

@danquirk So when @jonathandturner said at http://blogs.msdn.com/b/typescript/archive/2015/03/05/angular-2-0-built-on-typescript.aspx

We've also added a way to retrieve type information at runtime. When enabled, this will enable developers to do a simple type introspection.

he meant metadata generated only for decorators, not full-scale reflection (for all public symbols, for example)?

mhegazy commented 9 years ago

he meant metadata generated only for decorators, not full-scale reflection (for all public symbols, for example)?

correct.

sophiajt commented 9 years ago

What he said :)

remojansen commented 8 years ago

I'm happy to see:

A few notes on metadata:

  • 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".

From https://github.com/Microsoft/TypeScript/pull/2589

What other metadata are you planing to support in the future? There are a few things that I would like to see:

Of course "design:implements" would require the interface names to be serialized using the interfaces names not Object (or complex type serialization). The same any case in which an interface is serialized.

mhegazy commented 8 years ago

Parameter name metadata uses the metadata key "design:paramnames".

this sounds interesting. and should be doable

The same any case in which an interface is serialized.

Interfaces are not serialized today. only names with a value attached to them (i.e. classes) and built-ins are serialized.

remojansen commented 8 years ago

About:

Interfaces are not serialized today

I'm aware of a gist (by Ron Buckton) about complex type serialization, is that going ahead? Are you guys considering adding interface serialization support at some point?

christyharagan commented 8 years ago

As mentioned in my original post, I've got a project called typescript-schema that provides this kind of serialisation, as well as a reflective type model:

It has two types of model: A serialisable model, and a runtime reflective model.

It works by: AST -> Serialisable Model -> Runtime reflective model

It currently only supports 1.5, and only exported types. It's not 100% complete, but is able to process the entire TypeScript library itself, and the node type library too (as well as a number of other large libraries).

What I'd really like is to understand is:

a) if the TypeScript team would want any of this code, and if so b) what would be required to make it ready for inclusion? I.e.: as it stands it's largely standalone (and probably doesn't follow all the code standards)

To give you an example of it at work, imagine this TypeScript code:

import {X} from 'my-module'
export interface MyInterface<T> {
  f(x: X, s: string):T
}

This will serialise to:

{
  "instanceType": {
    "typeKind": "COMPOSITE",
    "members": {
      "f": {
        "optional": false,
        "type": {
          "typeKind": "FUNCTION",
          "parameters": [{
            "name": "x",
            "type": {
              "module": "my-module"
              "name": "X"
            },
            {
              "name": "s",
              "type": {
                "typeKind": "PRIMITIVE",
                "primitiveTypeKind": "STRING"
              }
            }],
            "type": {
              "module": "@",
              "name": "T"
            }
          }
        }
      }
    }
  }
}

This can then be converted into a reflective model, which can be used like:

  let myInterfaceConstructor:InterfaceConstructor //Called an interface constructor because it has type parameters

  myInterfaceConstructor.parent // The parent module or namespace
  myInterfaceConstructor.name // "MyInterface"
  myInterfaceConstructor.typeParameters[0].name // "T"
  myInterfaceConstructor.instanceType.members['f'].type.name // "T"
  myInterfaceConstructor.instanceType.members['f'].parameters[0].name // "x"

You can then "close" that reflective interface by providing a type argument, and the output will be the corresponding interface with all type parameters correctly substituted:

  let myInterface = closeInterfaceConstructor(myInterfaceConstructor, [someType])
  myInterface.typeConstructor // myInterfaceConstructor
  myInterface.instanceType.members['f'].type // someType

To check it out, go to:

typescript-schema

and

typescript-package (which provides the code for converting a package of TypeScript into the serialisable form - including typings and nested packages via the node resolve algorithm)

remojansen commented 8 years ago

When I asked for a "design:paramnames" metadata key.

@mhegazy said:

this sounds interesting. and should be doable

So I have created a new issue to request it: https://github.com/Microsoft/TypeScript/issues/4905

asvetliakov commented 8 years ago

Interfaces are not serialized today. only names with a value attached to them (i.e. classes) and built-ins are serialized.

What if you guys will introduce new language semantic? Something like realtime interface Name {...} which will remain realtime (with empty method bodies for easing mocking in testing frameworks)? I doubt the interface serialization will be implemented somewhen, since there is a much code written which assuming that interfaces are not realtime objects.

I forced to use classes as interfaces now to make DI work

mikehaas763 commented 8 years ago

Is this issue getting at being able to do something like the following for using interface annotations as DI tokens?

interface IAlgorithm {
    execute(): void; 
}

class FancyAlgorithm implements IAlgorithm {
    execute(): void {
        let i = 123 * 456;
    }
}

class AlgorithmExecuter {
    constructor(private algorithm: IAlgorithm) { }

    execute(): void {
        this.algorithm.execute();
    }
}

class Program {
    main() {
        // DI API based on autofac DI for .NET
        // container could be populated any number of ways just imperative for illustration
        let builder: Container = new ContainerBuilder();
        builder.RegisterType(FancyAlgorithm).As(IAlgorithm);
        builder.RegisterType(AlgorithmExecuter).AsSelf();

        // now invoke the composition root
        let executer = builder.Resolve(AlgorithmExecuter);
        executer.execute();
    }
}

The specific reason I'm asking is because when using a DI system for TypeScript that uses type annotations as metadata (such as what exists for Angular 2) this is currently not possible because no runtime "Reflect" unique metadata is emitted for interfaces. In DI circles this is basically the way DI based apps are built. It's extremely powerful and makes for very flexible and easily testable apps.

If this issue doesn't involve this kind of metadata please let me know as I'd like to create a separate proposal issue. :smile:

mhegazy commented 8 years ago

@mikehaas763 nothing stops you today from doing the same thing but using strings in place of types. i am assuming this would be sufficient. your DI system needs to have a key to store and locate these, if you use strings, then nothing in this issue should block you.

@export("IAlgorithm")
class FancyAlgorithm implements IAlgorithm {
    execute(): void {
        let i = 123 * 456;
    }
}
mikehaas763 commented 8 years ago

@mhegazy I don't understand what you're trying to portray with that code snippet. What I would like to do is be able to register an interface type as a key in a DI container. Can you elaborate?

mhegazy commented 8 years ago

I don't understand what you're trying to portray with that code snippet. What I would like to do is be able to register an interface type as a key in a DI container.

I do not know much about autofac, so my comments are assuming you have a central registry that records mapping between a type and an exported entity, something a la MEF.

I am also assuming you need a key, and you want to use the name of the interface as key, all what i am saying is you can do that by making the key as a string (in this case using the name of the interface, but it should be something more unique).

the export decorator i am using refers to something like MEF ExportAttribute, in JS terms this would be something that takes a key (interface name) and a value (a class constructor), and keeps them in the list (along with arguments to the constructor possibly). that is basically will call your builder.RegisterType(FancyAlgorithm).As(IAlgorithm); except that it will be somethign like builder.RegisterType(FancyAlgorithm).As("IAlgorithm");

later on your call to builder.Resolve("IAlgorithm"); and that would return you all constructors with this key.

mikehaas763 commented 8 years ago

UPDATE: I wrote this before you replied above. Reading your response now.

@mhegazy A problem with using strings in place of types is that you lose the automatic given uniqueness of that type. Two different libraries may have an IAlgorithm interface type and both may need to be registered with a single DI container. That's easy enough to do like so if the compiler generated some sort of unique metadata token to help guarantee uniqueness and a fake import could somehow still work at runtime. So something like the following:

import IAlgorithm as ILibAAlgorithm from './libA';
import IAlgorithm as ILibBAlgorithm from './libB';
import AImplementation from './a';
import BImplementation from './b';

builder.RegisterType(AImplementation).As(ILibAAlgorithm);
builder.RegisterType(BImplementation).As(ILibBAlgorithm);

The very nature of the two IAlgorithms being two different things is enforced by them being defined separately in separate modules. I understand that this is not possible today because there is no IAlgorithm to import at runtime (or do I have this wrong?). What I'm saying is that this would be a nice feature to have in TS. I'm speaking for myself now but also for the swaths of developers that I guarantee will reiterate wanting to see the same capability as TS becomes used more.

So either I have this completely wrong and it's already possible :smile: or if not it would be awesome if we could start talking about what the actual implementation would look like and make it a formal proposal for TS or there is just better ways to do this and I'm bringing over too much cruft to TS from Java/C# etc.

UPDATE: I finished reading your reply above. Yes you can assume there is a registry (the container in autofac). I get that it would be possible by registering a type against a string key in a container but do you see the concerns I have with that around uniqueness and robustness?

mikehaas763 commented 8 years ago

I am also assuming you need a key, and you want to use the name of the interface as key

I don't want to use the string name of the interface as a key, I want to use the interface itself as a key. This is possible with concrete types because they exist at runtime. Interfaces don't exist at runtime and obviously for good reason but it would be nice to have this sort of metadata for interfaces at runtime.

One implementation that may work is to compile the interface to an empty lightweight concrete type so it can be used at runtime.

// for example
export default interface IFoo {
    foo(): void;
    bar(): void;
    lorem(): void;
    ipsum(): void;
}
// could be compiled to (depending on the target but in this case ES6)
export default class IFoo {}

That way, IFoo is a guaranteed unique key at runtime.

BobbieBarker commented 8 years ago

I think Mike's point addresses a real concern for serious application development which utilize frameworks and Dependency Injection systems. With out something like Mike's proposal in place DI systems typically work as you suggest on strings or tokenized versions of the strings. In a non TS world, that works for the most part. But in a TS/typed universe I think we should be including and utilizing types where possible in our DI systems.

mikehaas763 commented 8 years ago

@mhegazy Do you think I should start a new issue to propose/track this?

I'd like to get more people's thoughts on this. After stepping away I think the most seamless way to do this would be like I mentioned above is to compile an interface type to a "class" type. This is already done with abstract class types.

Maybe a compiler option such as --runtimeInterfaces? I realize that this in a way is a huge change because the spec is explicit that interfaces do not exist at runtime. I know that abstract classes are now supported. Am I forced as a developer using DI to now use abstract classes wherever I would have previously used just an interface?

mhegazy commented 8 years ago

I mentioned above is to compile an interface type to a "class" type. This is already done with abstract class types.

I am not sure i understand what you mean by "compile" to "class type". interfaces do exist in a different space (type space), and emit a value for them would cause problems when it comes to merging (see declaration merging docs for more information).

I want to use the interface itself as a key. This is possible with concrete types because they exist at runtime.

one thing to note is TypeScript's type system is structural, it is not nominal (like other languages such as C# and Java with DI systems). so the interface name, or declaration is of no consequence here; consider an example with two different classes looking for interfaces with different names, but comparable structures, do you want your DI to find them or not?

interface Person {
     name: string;
     title?: string;
}

interface SomethingWithAName {
    name: string;
}

class C {
     constructor(c: SomethingWithAName) {}
}

var p: Person;
new C(p); // this is fine, the two interfaces are valid

then what would you do with things that do not have a name?

class C {
     constructor(a: {name: string}) {}
}

or type aliases?

type myType = string;
class C {
     constructor(a: myType) {}
}

or more complex type operators:

type myType = string;
class C {
     constructor(a: (string | { name: string }) & EventTarget) {}
}

Obviously a typical DI system from a language with nominal type system like C# would not fit here with no compromises. i would say you will have to limit the set of supported language constructs to allow your DI system to work, i.e. say only classes are supported by this system, interfaces, structural types, etc will not be allowed.

If you agree with this proposition, then it should just work with classes. classes have values, and can be used at rumtime as keys to your DI system. and that should be easy to model today using decorators, e.g.:

    // use an abstract class instead of an interface
    abstract class Algorithm {
        abstract execute(): void;
    }

    @exportAs(Algorithm)   // use the class constructor as a key for your type lookups
    class FancyAlgorithm implements Algorithm { // use implements here, so no runtime changes (i.e no calls to __extend)
        execute(): void {
            let i = 123 * 456;
        }
    }

    abstract class Class { }

    function exportAs<T extends Class>(typeObject: T) {
        return function (target: T): void {
            // wire the export
            builder.RegisterType(target).As(typeObject);
        }
    }

    // later at runtime you would do:
    let executer = builder.Resolve(Algorithm); // should find FancyAlgorithm
asvetliakov commented 8 years ago

Obviously a typical DI system from a language with nominal type system like C# would not fit here with no compromises. i would say you will have to limit the set of supported language constructs to allow your DI system to work, i.e. say only classes are supported by this system, interfaces, structural types, etc will not be allowed. If you agree with this proposition, then it should just work with classes. classes have values, and can be used at rumtime as keys to your DI system. and that should be easy to model today using decorators, e.g.:

Using abstract classes as interfaces approach works not very well for DI. Any abstract methods will be omitted from compiled JS, so it's not possible to create tests stubs from abstracted classes (for ex. using sinon.createStubInstance()). Also the compiler doesn't emit any runtime checks to prevent creation abstract classes at runtime. If you forgot to bind implementation to abstract class acting as interface, it will create empty object of your interface instance, instead giving you error:

abstract class TestInterface { ... }

class TestController { 
   @Inject
   public myService: TestInterface;
}

// Forgot to bind any implementation to TestInterface
let controller = container.resolve(TestController); // Doesn't throw any error since abstract class could be created at runtime.

I forced to use now these ugly constructions:

class Algorithm {
    constructor() {throw new Error();}
    public execute(): void {}
    public calculate(): number {return}
}

class MyAlgorithm implements Algorithm { .... }

With this, runtime checking and mocking methods (like sinon.createStubInstance()) will work correctly.

mikehaas763 commented 8 years ago

As an FYI, I was just suggesting compiling an interface to a type as one specific way to solve the problem of providing this sort of interface metadata. It could be handled other ways.

interfaces do exist in a different space (type space)

I'm aware. I suggested this purely as a means to be able to use interfaces in a meta way at runtime by just compiling it to a value type. I'm not saying it's the only way or even the proper way.

and emit a value for them would cause problems when it comes to merging

Why would this cause issues with merging? Merging does occur when an interface is defined in separate modules does it?

I'm aware that the type system is structural which I admit makes it harder for me to reason about this.

then what would you do with things that do not have a name?

If I was declaring the type annotation of something with a literal, than I wouldn't expect to be able to use that as a key in my DI system.

If you agree with this proposition, then it should just work with classes. classes have values, and can be used at rumtime as keys to your DI system

The problem with depending on classes is that it blatantly violates the dependency inversion principle: "Depend upon Abstractions. Do not depend upon concretions.". I can see people saying "see, just use an abstract class for that". Well, if I just end up using an abstract class just like an interface, what is the point of having interfaces in TS?

At the end of the day this conversation is just about possibilities to provide a means to an end and allowing something that makes development better and easier. That end being I want to be able to continue to program like the following snippet but also have dependencies injected as expected based on the type annotation that is already specified (IDependency).

class Foo {
    constructor(dependency: IDependency) {}
}

In the meantime I had planned on just using abstract classes as interfaces (like in the following snippet), but will have to look more closely at the problems that introduces that @asvetliakov mentioned above.

abstract class IAlgorithm {
    abstract execute(): void;
}
asvetliakov commented 8 years ago

Ideally it will be cool if you guys implement something like that:

interface realtime MyInterface {
    public method1(): void;
    public method2(): string;
}

Which will be compiled to:

function MyInterface() {
    throw new Error("Not possible to create MyInterface instance");
}

MyInterface.prototype.method1 = function () { throw new Error("Not possible to call interface method MyInterface::method1"); }
MyInterface.prototype.method2 = function() { throw new Error("Not possible to call interface method MyInterface::method2"); }

This will not interfere with existing interfaces in type scope and people will be able to use DI as it was intended. Or at least provide runtime constructor checking & empty function stubs for abstract classes & methods

mikehaas763 commented 8 years ago

@asvetliakov I like where you're going with that but at the same time IDK if I like that it has to be explicitly marked. If I'm implementing an interface that some 3rd party lib has provided, then I don't have the ability to mark that interface.

asvetliakov commented 8 years ago

@mikehaas763 Don't see any issues. 3rd party libs are usually declaring variable (with interface type) if exporting something, so there will be runtime information and you can use this variable as key for DI.

mikehaas763 commented 8 years ago

@asvetliakov I mean them declaring an interface to use as in the concept of inversion of control. They define an interface that a consumer implements and passed back into their lib.

mhegazy commented 8 years ago

Why would this cause issues with merging? Merging does occur when an interface is defined in separate modules does it?

now you are talking about limiting it to only module scopes. that was not in the OP.

Well, if I just end up using an abstract class just like an interface, what is the point of having interfaces in TS?

interfaces in TypeScript are design-only constructs, just like the rest of the types. using these constructs in the value space is not a TS design goal.

mhegazy commented 8 years ago

talking with @rbuckton today, here is another option that works today and does not use classes:

   const IAlgorithm = new Symbol("IAlgorithm"); // unique identifier for the interface
   interface IAlgorithm {
       execute(): void;
    }

    @exportAs(IAlgorithm)   // the interface and the var are merged
    class FancyAlgorithm implements Algorithm { 
        execute(): void {
            let i = 123 * 456;
        }
    }
remojansen commented 8 years ago

@mhegazy Your approach is good because you only have one magic string:

declare class Symbol {
    constructor(a: string);
};

function bind<T>(a : Symbol, b : T) {}
function resolve<T>(a : Symbol) {}

function inject(...types : Symbol[]){
    return function(target : any) {

    }
}

const IFoo = new Symbol("IFoo");           // The only magic string in the whole process

interface IFoo {}
class Foo implements IFoo {}

@inject(IFoo)                              // No more magic strings when declaring injections
class Test {
    constructor(algorithm : IFoo) {

    }
}

bind<IFoo>(IFoo, Foo);                     // No more magic strings when declaring bindings
var foo = resolve<IFoo>(IFoo);             // No more magic strings when resolving a dependency

I think that we need to come up with some way to enable this without having to do extra work. When I say extra work I mean declaring magic strings or symbols. This are just "manual metadata hacks" metadata should be generated by the compiler.

I don't know if it is the best way but we could use a new compiler option emitInterfaceSymbols.

When the emitInterfaceSymbols flag is used and we declare an interface:

interface IFoo{}

The symbols are generated by the compiler:

const IFoo= new Symbol("IFoo");

At design time we would need to use declare var IFoo : Symbol; to avoid compilation errors:

// the run-time symbol is generated by the compiler but we need to declare at design-time
// is extra work but at least it is not a magic string...
declare var IFoo : Symbol; 

declare class Symbol {
    constructor(a: string);
};

function bind<T>(a : Symbol, b : T) {}
function resolve<T>(a : Symbol) {}

function inject(...types : Symbol[]){
    return function(target : any) {
        // ...
    }
} 

interface IFoo {}
class Foo implements IFoo {}

@inject(IFoo)
class Test {
    constructor(algorithm : IFoo) {

    }
}

bind<IFoo>(IFoo, Foo);
var foo = resolve<IFoo>(IFoo);

It would be cool if the IDE is able to identify when we are declaring an interface and using the emitInterfaceSymbols flag and it would automatically assume that the a symbol with he same name as the interface is declared. So we don't need to type the line below to avoid compilation errors:

declare var IFoo : Symbol;
pcan commented 8 years ago

Hi everybody, I would just add my 2 cents to the discussion. Some time ago I started working on a type serializer that is based on rbuckton's proposal types.d.ts; currently it's in a prototyping stage and not published yet, but I have uploaded some sample code in order to discuss a possible way of implementing full type serialization. I know, there could be many drawbacks when doing this, but in some cases could be useful to have all information about implemented interfaces, generic types and so on. I have included some open points in the readme, I would be glad if you took a look at it here.

thomas-darling commented 8 years ago

Ok, this discussion is definitly interresting, and I will openly admit that I'm no expert on this, yet.

However, reading through it all, it seems like the thing most people are asking for is actually something very simple - just compile interfaces to an empty constructor function, so we have something at runtime we can use as a key when doing dependency injection. Thats it. No theoretical discussion of type systems and stuff there. We just need that key.

To me, this sounds like something that should be fairly simple to implement, and it would be consistent with how abstract classes are currently compiled. Abstract classes do already exist at runtime, and we just need interfaces to be compiled in exactly the same way. I really don't see the difference here, and the DI problem everyone is talking about is very real - when building real-world apps, we absolutely need the ability to use interfaces as keys for DI.

I think it would be a good idea to track this in a separate issue, and then deal with the the whole question of reflection and type model separately, as it is really a completely different issue. I therefore suggest that #3060 is reopened, as it represents exactly what is being asked for here.

Unless of course I'm missing the point, and there is a good reason why this is not technically possible? I really see no reason why we should have to deal with magic strings anywhere to acheive this.

mhegazy commented 8 years ago

However, reading through it all, it seems like the thing most people are asking for is actually something very simple - just compile interfaces to an empty constructor function

So just write your interfaces as classes. that should be a simple regexp.

mikehaas763 commented 8 years ago

So just write your interfaces as classes. that should be a simple regexp.

Then what's the point of having interfaces in TS in the first place? Why not just stop using interfaces in your codebase, deprecate the construct from the lang, and move on?

However, it is what I'm doing now. I think many people (including myself earlier on) didn't realize that you can implement constructs other than interfaces like class Foo implements AbstractOrConcreteType{}. A stringent code review process can help ensure that no implementations are allowed into abstract classes and we just treat the abstract classes purely as an interface. It does still come back to the idea of why even have interfaces in TS going forward?

mhegazy commented 8 years ago

Then what's the point of having interfaces in TS in the first place? Why not just stop using interfaces in your codebase, deprecate the construct from the lang, and move on?

interfaces, as well as type aliases, do not exist at run-time. They are merely a design time construct. this is the whole point of TypeScript :) a design time tool to allow you to make sense of your code with no runtime cost.

Classes, variables, etc.. are runtime constructs by definition. So I am confused why you want interfaces to have a runtime manifestation, but at the same time do not use classes.

thomas-darling commented 8 years ago

If you want a specific use case for this, the dependency injection in the Aurelia framework is a very good example. This works today if IEngine is an abstract class, and would work for interfaces too, if only the interface existed at runtime.

var container = new Container(); 
container.registerSingleton(IEngine, Engine); 

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

The problem is, I can't just replace my interfaces with abstract classes, as that would prevent my implementation from inheriting from anything other than the interface. Maybe I want my Engine to inherit from CarPart or something - interfaces are about composition, not inheritance.

Is there any technical reason why interfaces cannot be compiled in the same way abstract classes are? Of course, in the IDE the two things are quite different, but we just need the same output in JS.

mikehaas763 commented 8 years ago

interfaces, as well as type aliases, do not exist at run-time. They are merely a design time construct. this is the whole point of TypeScript :) a design time tool to allow you to make sense of your code with no runtime cost.

I know this and you've reiterated it to me several times. The point comes down to DI. If I can't use interfaces for pragmatic use cases such as with a DI framework, how much value do they really add. A developer (the consumer) should not be thinking about whether certain constructs are available at runtime or not when they are just trying to get work done.

thomas-darling commented 8 years ago

I absolutely agree.

@mhegazy If an abstract class has a runtime manifistation, why should it be any different for an interface? I realize typescript is considered a typed superset of ecma script, but interfaces and DI is absolutely essential to building real world apps, and I see no reason why typescript should not make this possible.

mikehaas763 commented 8 years ago

interfaces are about composition, not inheritance.

I see your point. But generally interfaces are about enabling one to compose, but really anything that implements an interface etc does become a sub type of it. Therefore it's kind of inheritance too. :smile:

The problem is, I can't just replace my interfaces with abstract classes, as that would prevent my implementation from inheriting from anything other than the interface.

I'm not following why it's not technically possible?

abstract class Foo
{
    void DoFoo();
}

class Bar implements Foo
{
    void DoFoo() {}
}

someFunction(fooer : Foo){}
someFunction(new Bar());

Is there any technical reason why interfaces cannot be compiled in the same way abstract classes are?

Not technical reasons but it's a change to the spec and I think historically the reason has been about not shipping extra cruft to the browser.

pcan commented 8 years ago

Hi guys, I would just to point up that this kind of things IS already feasible (I did it, just I said previously), but it may be better if the compiler would be a bit more "extensible", so I should not have to wire up things in the compiler in order to emit interfaces, classes and other metadata; we could simply recall a compiler plugin to do this job. The main issue is not the extraction of metadata (I'm using the plain compiler API), but its "linking" to the emitted code.

@mikehaas763: abstract classes won't cover the case of mixin interfaces. A Dependency Injection mechanism could be driven by more interfaces, and still injecting the same singleton object, just because it implements more than one interface.

thomas-darling commented 8 years ago

Hmmm, ok, I think I might have missed the point that a class can implement other classes like that - that's really interresting, and it does in fact make what I'm talking about possible using abstract classes.

However, it does feel extremely strange to use an abstract class as if it was an interface - and the fact that it is possible to e.g. define actual methods, and not just method signatures, in an abstract class, that just feels plain wrong when what I'm really trying to do, is to define a contract the class must implement.

While I guess abstract classes can be used as a workaround like this, I still think interfaces should have a runtime manifistation, and I'm pretty sure that's what 99% of developers expect when they start using typescript. Having a class implement an interface makes the intent much clearer, compared to abusing abstract classes as if they were interfaces (i.e. contracts with no behavior), and then having code reviews to avoid getting method bodies and other mistakes in there.

I really think you guys should reconsider this, and emit code for interfaces too - again, I don't see why it should be any different from abstract classes. It would just mean that interfaces would work the way pretty much everyone expects them to - otherwise I really see no value in the interface construct at all.

rbuckton commented 8 years ago

I am firmly of the belief that any DI system for JavaScript is better served by using unique exported string or symbol values. I've been thinking more and more about adding special compiler-specific decorators that could target things like interfaces and add metadata to the classes that implement them.

thomas-darling commented 8 years ago

Just to clarify, I don't really have a strong preference as to whether an interface is emitted as a constructor function or any other unique value - I just used the abstract class as an example due to the similarity between the two, but I guess emitting a Symbol might technically be more correct, as what we're talking about here is an identity and not something that should ever be instantiated - but really, anything that can be used as a unique key for DI would make me a very happy developer :-)

@remojansen, the solution you suggested, where setting an emitInterfaceSymbols option will cause symbols to be emitted for interfaces, that sounds a lot like the solution I would like to see - as long as it doesn't require any code changes :-)

thomas-darling commented 8 years ago

Just a quick note here - isn't the question of whether something should be emitted to represent an interface very similar to the case of const enum declarations?

Normally a const enum declaration would be compiled away, but here we have the --preserveConstEnums options to ensure it is preserved in the generated code. https://github.com/Microsoft/TypeScript/wiki/Compiler-Options

To me, this appears to be a very similar issues... maybe a preserveInterfaces option could be added, so we can reference an interface just as if it was a class, thus enabling the DI scenario discussed here?

Just like code referencing a const enum may appear to work while coding, and then breaks at runtime as described in https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#94-constant-enum-declarations, the code in my comment https://github.com/Microsoft/TypeScript/issues/3628#issuecomment-163349629 compiles just fine and looks like it should work as expected - but then it all blows up at runtime due to the missing runtime representation of the interface. This is a highly frustrating and unexpected experience, especially for developers new to typescript, and I still believe this is a very legitimate use case for interfaces and therefore something that should be enabled.

remojansen commented 8 years ago

@rbuckton I think most of us agree that using string (rather than complex type serialization) is the right balance between what we want to achieve and run-time overhead. In my opinion runtime type assertion is a bad idea.

I saw your comment about interface names not being enough information. This causes the following issue in my project:

interface IA {
  x : number;
}

interface IB {
  x : number;
}

class A implements IA {
  x : number;
}

class B implements IB {
  x : number;
}

kernel.bind(new Binding<IA>("IA", A));
kernel.bind(new Binding<IA>("IA", B)); // OK because structural typing

I personally don't mins this issue because:

At the end of the day what really matters when injecting a dependency is its interface not its name.

I'm not planing to do auto-wiring so I can live without 100% identifiable interfaces.

At the moment developers need to manually type the names of the interfaces to be injected:

@Inject("FooInterface", "BarInterface")
class FooBar implements FooBarInterface {
  public foo : FooInterface;
  public bar : BarInterface;
  public log(){
    console.log("foobar");
  }
  constructor(foo : FooInterface, bar : BarInterface) {
    this.foo = foo;
    this.bar = bar;
  }
}

Magic strings are error-prompt and not refactoring-friendly. It would be great if we could leave the compiler add those strings to the metadata using:

Reflect.getMetadata("design:paramtypes", FooBar); 

It should return ["FooInterface","BarInterface"] so users can forget about magic strings.

Note that we need actual strings not functions named like the interfaces because we need to be able to use JS compressors.

I don't think using Symbols is a good idea because I think it could create some problems:

var f2 = Symbol("FooInterface");
var f = Symbol("FooInterface");
f == f2 // false

Specially if we are consuming third party code with metadata.

rbuckton commented 8 years ago

The problem with Reflect.getMetadata returning an array of strings is the possibility of collisions. Consider this:

// a.ts
export interface Service { x: number; }
// b.ts
export interface Service { y: number; }
// main.ts
import * as a from "a";
import * as b from "b";
class ServicesImpl implements a.Service, b.Service {
}
Reflect.getMetadata("design:paramtypes", ServicesImpl); // ["Service", "Service"]

Strings are useful if they are unique, and there's no reliable way for TypeScript to generate a unique string that doesn't change per build. It becomes a refactoring and code reorganization hazard.

A better approach would be this:

// a.ts
export interface Service { x: number; }
export const ServiceName = "urn:a:Service";
// b.ts
export interface Service { y: number; }
export const ServiceName = "urn:b:Service";
// main.ts
import * as a from "a";
import * as b from "b";
import { provide, getService } from "some-composition-framework";
@provide(a.ServiceName)
@provide(b.ServiceName)
class ServicesImpl implements a.Service, b.Service {
}

const aService = getService<a.Service>(a.ServiceName);
const bService = getService<b.Service>(b.ServiceName);

Symbols are event better, as they can guarantee uniqueness:

// a.ts
export interface Service { x: number; }
export const ServiceKey = Symbol("Service");
// b.ts
export interface Service { y: number; }
export const ServiceKey= Symbol("Service");
// main.ts
import * as a from "a";
import * as b from "b";
import { provide, getService } from "some-composition-framework";
@provide(a.ServiceKey)
@provide(b.ServiceKey)
class ServicesImpl implements a.Service, b.Service {
}

const aService = getService<a.Service>(a.ServiceKey);
const bService = getService<b.Service>(b.ServiceKey);
pcan commented 8 years ago

The problem with Reflect.getMetadata returning an array of strings is the possibility of collisions.

I don't know if this option has already been discussed, but... What about a fully qualified name? the tsconfig.json (in the "root package") already contains the list of each ts file, which could be used as the "base name" for interfaces.

Example:

// common/baz/StringUtils.ts
export interface Service { x: number; }

// foo/bar/AnotherService.ts
export interface Service { y: string; }

// Test.ts
import * as a from "common/baz/StringUtils";
import * as b from "foo/bar/AnotherService";

class ServicesImpl implements a.Service, b.Service {
}
Reflect.getMetadata("design:paramtypes", ServicesImpl); // ["common.baz.StringUtils#Service", "foo.bar.AnotherService#Service"]

Maybe this could be better than nothing :)

EDIT: the same strategy may be used for modules, for example:

// common/baz/StringUtils.ts
export module foo {
    export interface Service { x: number; }
}

// emitted as: "common.baz.StringUtils#foo.Service"
pcan commented 8 years ago

If someone is still interested in reflection capabilities, I just released an enhanced version of the compiler that emits classes/interfaces metadata for runtime type checking. The compiler injects some synthetic instructions in the AST just after the parsing phase, so the metadata is available also for intellisense/autocompletion. I haven't tested this with Visual Studio, but with Atom it works really well. Keep an eye on this.

remojansen commented 8 years ago

@pcan great work :+1: we really need better reflection in TypeScript :cry: I hope to see something in the official compiler soon. We could create so many amazing tools.

felixfbecker commented 8 years ago

I just implemented a proof-of-concept for using decorators to define Models in sequelize: https://github.com/felixfbecker/sequelize-decorators

If you don't know, Sequelize is the most popular ORM for NodeJS. In version 4 we are enabling users to use classes to define their models. This is great for TypeScript, because we can use decorators to define the attribute metadata, like what column type it should be, if it should allow NULL, if it should be a primary key, if it should have a unique constraint etc etc.

I actually implemented it in a way that you can leave out the type for the types that are already emitted under design:type. So if the property is a Date, you don't need to pass to the @Attribute decorator that the sequelize / database data type should be DATE, it is inferred. Similarly, Buffer is inferred as BLOB. But it is very limited atm, and I see so much more potential. For example:

Basically, it should be possible to get all the info TypeScript has about a property. Which leads me to the thought, by can't TypeScript simply expose the AST node? I'm sure it already has structures that contain all the necessary information.

remojansen commented 7 years ago

Hi guys! { structuralTypes: false } in tsconfig.json is never gonna happen right? 😢