Typescript: Feature Request / Proposal: Traits

Created on 30 Jul 2014  Â·  31Comments  Â·  Source: microsoft/TypeScript

ORIGINALLY PROPOSED IN http://typescript.codeplex.com/workitem/838

Traits, as "compile-time" partial classes, would perfectly fit with TS ideology and resolve problems of multiple inheritance/mixins.

Traits in Scala: http://en.wikibooks.org/wiki/Scala/Traits
Traits in PHP: http://php.net/trait

I propose minimal traits similar to PHP implementation.

trait FooTrait {
   // same as class definition, but no constructor allowed, e.g.:
   public foo() {
       return "foo";
   }
   private bar() {
       return "bar";
   }
}

class Foo {
    import FooTrait; //not insisting on exact keyword and syntax, just smth to start with

    // ...
}

class Bar {
    // rename methods:
    import FooTrait(foo => tfoo, bar => tbar);

    // and to include another trait, there is another import line:
    // import BarTrait;

    // ...
}

The code above could be compiled to JS below (can be optimized, just showing the main idea):

var FooTrait = (function () {
    function FooTrait() {
        throw "Cannot instantiate trait FooTrait";
    }
    FooTrait.prototype.foo = function () {
        return "foo";
    };
    FooTrait.prototype.bar = function () {
        return "bar";
    };
    return FooTrait;
})();

var Foo = (function (_super, FooTrait) {
    function Foo() { }
    Foo.prototype.foo = FooTrait.prototype.foo;
    Foo.prototype.bar = FooTrait.prototype.bar;
    return Foo;
})(undefined, FooTrait);

var Bar = (function (_super, FooTrait) {
    function Bar() { }
    Bar.prototype.tfoo = FooTrait.prototype.foo;
    Bar.prototype.tbar = FooTrait.prototype.bar;
    return Bar;
})(undefined, FooTrait);

Unresolved name conflicts should raise a compile time error.

I think it would be a great advance for the language. But the proposal has more than one year made (in codeplex) ​​and has not yet been implemented.

Declined Suggestion

Most helpful comment

Mixins is an ugly way that feels like we are using ecmascript 3. Traits would allow us to use a more object oriented clean syntax.

All 31 comments

I think Mixins are relatively similar to traits, however, there are a few disadvantages (at least as it's described in the handbook):

  1. You have to redeclare class members and methods in the implementing class to satisfy the compiler.
  2. You have to "apply" the mixins (i.e. copy the class members and methods from the mixin class) using a custom function.

Are the points 1 and 2 by design or are these feature yet to be implemented?

Mixins is an ugly way that feels like we are using ecmascript 3. Traits would allow us to use a more object oriented clean syntax.

This would certainly be awesome. I don't care how it's done, but this would be great--I use TS mainly in games and engines, and the ability to "mixin" methods from a trait would be great for actors/objects in games (for example, an object importing an EventDispatcher but inheriting from a base class).

class Actor {
  x: number;
  y: number;
  // ... etc
}

class Player extends Actor {
  import Traits.Eventing.EventDispatcher;

  update(engine) {
    // using my event dispatcher trait
    this.publish(new Event(...));
  }
}

I don't know how this will work with TS to allow using traits like interfaces (maybe simply traits can implement interfaces).

class SomeClass {
  public listen(emitter: EventDispatcher) {
    emitter.on("eventName", this.handleSomeEvent);
  }
  handleSomeEvent(args) {
  ...
  }
}
new SomeClass().listen(player);

Traits as part of Typescript and its type system would be really helpful to use the DCI programming paradigm with (http://en.wikipedia.org/wiki/Data,_context_and_interaction).

I would prefer a trait solution, where the trait is an interface with a default implementation. Such an interface alone would not generate any JS code on compilation.
Like in Scala, such a trait-interface can now be used in two ways:

  • The interface can be implemented by a class: The class will get the methods "mixed in" (like in @yasselavila code in the first post) .
    The class may need to implement methods which don't have a default implementation
  • The interface can be added to an object when constructing the instance.

Here is an example:

// Trait-Interface with default implementation
interface Customer {
    isGoldMember: false;
    public discountFactor(): number {
       return (this.isGoldMember ? 0.1 : 0); 
    }
}

// Trait-Interface is implemented
class MyGoldCustomer implements Customer {
       public isGoldMember() { return true; }
}

// Trait-Interface is used on instantiation
class Person {
      constructor(private name: string) {};
      public getName():String { return name; };
      // ...    
}

var p = new Person('John') with Customer; 
// p is of type Person AND Customer. The new object gets the methods "mixed in".
// The type of p is the same type as class X, if X is defined as "class X extends Person implements Customer"

The "with" syntax is borrowed from Scala. Of course some other syntax could be used for mixing trait interfaces into a new instance, e.g.:
new Person('John') & Customer;
This syntax would fit to union typers (#805). With traits we have another sort of union type, but the instance is implementing both types, not one of them.

I'm curious if this suggestion is compatible with the current Typescript type system. Any thoughts?

Also, without multiple inheritance or traits, not possible to use TypeScript with Dojo Toolkit. The whole library is built on mixins system. Of course, we can use default "dojo/_base/declare", but this way negates the benefits from classes and interfaces in TypeScript.

TypeScript team, are you guys open to receiving possible implementations of this feature?

We'd be open to implementations in the future but it's important that we first nail down a proposal we can all agree on. It'd be getting ahead of ourselves to start reviewing an implementation before we nail down what is and isn't the intended behavior here.

No problem. SitePen is currently working on defining some package specifications for Dojo and this is one of the areas that we are looking to improve, so once we get a bit further along and actually begin the work of defining technical proposals those will be brought here. A prototype implementation would be useful to inform that decision-making and may form a suitable foundation for a final implementation, but these are early days, we have no code yet. I just didn’t want to have anyone on this side start going down the road and then find out that there is no chance to accept this, or that after talking to @mhegazy about C3 linearization that you guys wanted to move in that direction instead.

@jbondc Great! Looking over what you are proposing so far I would probably start by just focusing on the compiler side of things and not even thinking about any runtime enhancements in order to keep the initial feature very clear and focused (like the OP).

I understand not wanting to introduce new reserved words that weren’t future reserved words in the EcmaScript spec but import class is very unfortunate. The strawman introduced a “trait” adjective for class (trait class), which is better.

I/others will have additional feedback over the coming months on this.

@jbondc, there is a similar proposal on the old codeplex site. What do you think about it?

Just a brief update for everyone watching: I should have a complete(ish) proposal to share in the next couple of days at the latest. Sorry for the delay.

Our proposal is now available at https://docs.google.com/a/sitepen.com/document/d/112Xw-t8eh-eFIdKhn7LeDbGhO86XwlKv-Fwc6j9U-Oc/edit for discussion. (Please let me know if you prefer this to be in another format/location.) This proposal focuses purely on enhancing the compiler and doesn’t include any runtime enhancements.

Looking forward to your feedback and working with the rest of the team and community on refining these proposals into something that will be accepted into TypeScript.

@csnover +1... but ... what about use only 'trait' instead of 'traitclass'?

@csnover agreed with @csnover. trait class seems very...peculiar to me. Especially since it, in the end, shares very little in common with class.

The use of an adjective was taken from the ES strawman, I have no particular affinity for that grammar. This really is a first draft to get feedback so if people think the adjective is wrong then I’ll update the proposal until mostly everyone is happy :)

(I personally prefer multiple inheritance with C3 over traits since it covers more common use cases and is usually more intuitive for people that already know how single-inheritance works.)

TypeScript team, do you have any feedback or can you let me know when you might have time to review this proposal? Is there information that is unclear that needs to be clarified before additional feedback can be solicited?

I think the proposal is very clear. We're really busy wrapping up ES6 stuff at the moment so I can't give any hint of a timeline at this point.

This might qualify as a separated feature, but I am wondering can a runtime-only version of traits be implemented so that native objects can be augmented in a more OO fashion.

For example, say I define a "runtime trait" like

trait ChildList extends Element {
    get childList(): Element[] { /* iterate the HTMLCollection ... */ }
}

and then use it like (someElement with ChildList).childList....

Emit for the trait could be the same as the current compile-time version (i.e. as prototype properties in a Function). For the call site, Function.apply can be used.

In the type system, such trait can be treated like an interface. (With the same instanceof problem that plagues the current mixins and the trait proposal above.)

There can even be some Scala-inspired magic like implicit conversion (probably module-based to maintain sanity).

Thanks everyone for the feedback and sorry for the delay.

It would be nice to have a generic mechanism for combining types (like a with or +) but you would need to have a way to resolve conflicts which is isn’t possible with just a simple operator.

@jbondc

Added the 'constructor' option back.

Agree on the constructors in traits, that was a mistake, can you put a comment on the proposal so I can remember to fix it there?

I didn't understand the part 'They also enable automatic cross-cutting of concerns simply by inheriting multiple mixins, which is not possible with traits (in a traits model, the methods would conflict and could not chain together with super calls).'

OK, no worries, I will try to clarify in the proposal. To give a little more background here, one reason to prefer C3 over traits is to avoid having to do a lot of (keyboard) typing especially when you are composing things together. It also lets the consumer of a library avoid understanding the right method ordering for every conflict method in order for it to work.

So, as a concrete example, dgrid, a responsive data grid widget, allows users to enhance its functionality by composing together classes adding in mixins/extensions. Users start with a base Grid, add keyboard navigation by mixing in Keyboard, and add column resizing by mixing in ColumnResizer, etc. Even though some of these mixins implement the same methods, everything almost always Just Works transparently and the user doesn’t have to think about sorting out how the super-calls should be ordered. So they can write:

class MyGrid extends Grid, Keyboard, ColumnResizer {}

and they are finished and everything just works.

In contrast using traits there would be numerous method conflicts, buildRendering, destroy, postCreate, renderHeader so it would end up needing to be more like this:

class MyGrid extends Grid {
  use Keyboard {
    destroy as destroyKeyboard,
    postCreate as pcKeyboard,
    // ... more here ...
  };
  use ColumnResizer {
    destroy as destroyColumnResizer,
    // ... more here ...
  };

  destroy() {
    super.destroy();
    this.destroyKeyboard();
    this.destroyColumnResizer();
  }

  postCreate() {
    return this.pcKeyboard(super.postCreate());
  }
}

And then if you ever change the way your grid is constructed you’d have to go through and find and fix all these methods explicitly too. Lots of work compared to letting C3 take care of it for you! (Replace the method renaming with symbols to the traits and it’s the same problem.)

https://gist.github.com/jbondc/b09d56dc4b8ed6c43af6

From what I see you are introducing a conflict since you have traits A and B used by class C and trait A also extends trait B, how would this be resolved? I don’t see any conflict resolution in this feedback gist. C3 automatically fixes this ambiguity by reordering dependencies of all inherited classes.

Also, sorry, somehow I neglected to link to wikipedia on C3 linearization for more background on how the algorithm works (it’s pretty straightforward if you can get past the pseudo-code, which makes it look more confusing than it is!), I added the link in the proposal.

@jbondc

As long as the composing class implements all conflicting methods, there is no conflict reported by the compiler:

To be able to do this, programmer must know about all methods in all imported mixins and which of them are in conflict. Why he should if it is possible resolve this automatically like dojo/declare does (like in dstore example by @csnover)?

There's a lot of overlap here with mix-ins; it would be bad (or at least treacherous) to do both. With mix-ins being a potential part of ES7/ES8+, we want to take the runtime side of this very conservatively and get TC39 to agree on a proposal in this area first.

Features specific to the type system to support these patterns are still on the table, but we'll track those in other issues.

Is there an update on how/if mixins will become a first class capability of typescript rather than a bit of a hack with separated registration as it currently stands?

Is there an update on how/if mixins will become a first class capability of typescript rather than a bit of a hack with separated registration as it currently stands?

I am sure I will be corrected if I am wrong, but the TypeScript team have made it clear that because this is a domain that TC39 has indicated they will address, the team feel that this is something it doesn't feel it can introduce as it would likely cause incompatibilities with ES in the future.

The implications are, those of us who are passionate about it should be championing it within TC39. The concepts of mixins/traits were part of the originally Harmony proposals, but didn't make it into the spec. Also, as far as I am aware, there isn't any active advocacy on this specific area in TC39 at the moment. Once there is a proposal sufficiently through the process in TC39, then I believe the TypeScript team would be more than glad to implement it.

@kitsonk thanks for the update, I guess we just have to hope for TC39, I'll see if I can find any discussion on the matter. I feel at this point that traits are the only major capability missing - I'm currently managing a site with ~50k sloc TS, and the mixin registrations are somewhat unwieldy.

+1 for this feature

What's the status on this feature?

What's the status on this feature?

See https://blogs.msdn.microsoft.com/typescript/2017/02/02/announcing-typescript-2-2-rc/ ... basically they're not adding new language grammar, but by making some changes to the way Classes work, it's possible/easier to achieve now.

One powerful pattern that is possible with traits is this (similar to what @jbman mentioned):

trait AccountService {
  debit: (account: Account, amount: number) => Account;
  credit: (account: Account, amount: number) => Account;
  transfer(from: Account, to: Account, amount: number): [Account, Account, number] {
    return [this.debit(from, amount), this.credit(to, amount), amount]  
  }
}

This is not something that TC39 can help us with as in this particular form it's only relevant to statically typed languages. It can be done with abstract classes but then we lose the benefit of the mixin pattern as we move the problem into the inheritance area. Trait composition also makes pattern such as "cake" pattern possible.

+1

You can have a look at a simple library i wrote at TypeScript Mix I think it fits nicely into how you would want to use traits/mixins

Was this page helpful?
0 / 5 - 0 ratings

Related issues

uber5001 picture uber5001  Â·  3Comments

kyasbal-1994 picture kyasbal-1994  Â·  3Comments

blendsdk picture blendsdk  Â·  3Comments

DanielRosenwasser picture DanielRosenwasser  Â·  3Comments

wmaurer picture wmaurer  Â·  3Comments