Preferred mechanism for class combination

Topics: General
Nov 26, 2013 at 5:17 AM
Edited Nov 30, 2013 at 6:59 PM
Hi,

This is kind of a follow-up to https://typescript.codeplex.com/discussions/465035 since it sounds like TypeScript is going to be constrained in this area for at least a while. I’m hoping to get some feedback from TypeScript users and developers on the “best” approach for dealing with the case where you have two classes that you want to combine into one class.

For example, in Dojo, we have a Stateful class which enables accessors/mutators and watching for property changes in ES3 (get/set/watch methods), and we have an Evented class which enables listening for and emitting events on a specific object (on/emit methods). For an object that both uses accessors and also emits events, a normal construction would look like:

var StatefulEvented = declare([ Stateful, Evented ]);

In TypeScript, we can only inherit one. So what is the best solution here?

1:
interface IStatefulEvented extends IStateful, IEvented {}
var StatefulEvented:{ new (kwArgs:Object):IStatefulEvented; } = declare([ Stateful, Evented ]);
or 2:
class StatefulEvented extends Stateful implements IEvented {
  constructor(kwArgs:Object) {
    super(kwArgs);
    Evented.apply(this, arguments);
  }
  on(type:(target:any, listener:(event:Event) => void) => void, listener:(event:Event) => void):IHandle;
  on(type:string, listener:(event:Event) => void):IHandle;
  on(type:any, listener:(event:Event) => void):IHandle { return <IHandle> null; }
  emit(type:string, event:Event):void {}
}
StatefulEvented.prototype.on = Evented.prototype.on;
StatefulEvented.prototype.emit = Evented.prototype.emit;
or 3:
class StatefulEvented extends Stateful implements IEvented {
  constructor(kwArgs:Object) {
    super(kwArgs);
    Evented.apply(this, arguments);
  }
  on = Evented.prototype.on;
  emit = Evented.prototype.emit;
}
or 4:
class StatefulEvented extends Stateful implements IEvented {
  constructor(kwArgs:Object) {
    super(kwArgs);
    Evented.apply(this, arguments);
  }
  on(type:any, listener:(event:Event) => void):IHandle {
    return Evented.prototype.on.apply(this, arguments);
  }
  emit(type:any, event:Event):void {
    Evented.prototype.emit.apply(this, arguments);
  }
}
…or something else?

The first one doesn’t create a class that can be extended any more. The second one is gross for reasons that are hopefully obvious. The third one is gross like the second one, except also sets the on and emit methods on every instance. The fourth one is most like what one might expect from an unassisted mix-in in JavaScript.

Obviously I would really really like for TypeScript to have something built in that makes authoring classes at least as easy as it already is for Dojo users, but barring that at least coming up with a reasonably sane solution here for now could be helpful. Also keep in mind that some of these mix-ins could have 10 or more methods so reducing code duplication would be fairly important.

Thanks for your thoughts,
Coordinator
Nov 26, 2013 at 10:21 PM
This is one of those areas where being conservative around which abstractions we adopt for 1.0 definitely shows through. Extension methods(https://typescript.codeplex.com/workitem/100) and mixins have been suggested. It looks like the latter would fit what you're looking to do, namely, mix together two partial classes (or mixins) together into a new class declaration. This is definitely a powerful abstraction, and has a history in the JS community.

One solution, albeit ugly, would be to simulate multiple inheritance using subclasses of Stateful that include Evented's member. You could generate the matrix of such combinations, similar to your #2 and #4 but using classes. As we use structural inheritance, you wouldn't need to implement an interface.

The closest to what you're asking for that works today, aside from generating the mixins yourself, is #1. Here you would call your own mixin function and would cast the output of the mixin function to be the interface that extends both interfaces.

We're definitely looking into ways we can improve abstractions further, and make cases like this easier to do, after 1.0.
Nov 27, 2013 at 10:52 AM
Edited Nov 27, 2013 at 11:06 AM
Ideally, instead of providing a runtime feature that would create mixins or multiple inheritance, I would prefer a way to declare that a function returns a combination of type, and a less strict checking of what can be 'extended' :

  //mixin declarations 
  declare function createMixin<T,U>(a: T, b: U): mixin(T,U);
  class A {
    methodA() {}
  }

  class B {
    methodB() {}
  }

  var mix = createMixin(new A(), new B())
  mix.methodA();
  mix.methodB();

  //extending non-class object
  interface Foo { doSomething(); }
  declare var Foo : { prototype: Foo; new(): Foo };
  class Bar extends Foo {
  }
By introducing this kind of features typescript could improve greatly its flexibility, while still providing a strong type-checking. (in my opinion)
Coordinator
Nov 27, 2013 at 3:33 PM
I agree, something that could mix at the typesystem level would be powerful. We've already talked on the forums about an operator like ||, so you could say "number || string" to say that the type could be either. The trick is to come up with a way to describe things like this that doesn't complicate the type system. If we could come up with that, I think the next step might be to use annotations in a way very similar to your example:

class A {
   methodA() {}
}

class B {
   methodB() {}
}

@mixin(A, B)
class AB {}
Where the user could create a function like mixin, and AB would be passed to the function, and then assigned what came out, in a way played nicely with the type system. I'm not sure exactly how that would work, but if we did it right, then people could create their own extensions to the language, and would open the door to extension methods/mixins/etc
Jul 21, 2014 at 4:26 AM
@jonturner +∞