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

Mixin language support (with toy implementation) #2919

Closed
dbarbeau opened this issue Apr 25, 2015 · 49 comments
Closed

Mixin language support (with toy implementation) #2919

dbarbeau opened this issue Apr 25, 2015 · 49 comments
Labels
Discussion Issues which may not have code impact

Comments

@dbarbeau
Copy link

Hello TypeScripters.

I might be opening a can of worms which might bring an age of darkness upon us (but see PS). Anyway, I've done an attempt to add language support for mixins.

For the user, it is similar to the extends or implements keywords, and thus goes by mixes. The semantic intent of the keyword is that a Mixture is NOT a derived class of its Mixins, it just copies their static and dynamic properties.

Several mixins can be supplied, the order determines which property will finally be mixed (newer definitions shadow previous definitions).

What this does is roughly equivalent to http://www.typescriptlang.org/Handbook#mixins, except there is no need to copy attributes manually, so mixins become more manageable - hopefully.

class Base {
    say(hello:string) {
        console.log(hello);
    }
}

class Mixin1 {
    something : string = "Couac couac";

    fn1() {
            console.log(this.something);
    }
}


class Mixin2 {      
    something : string = "shadowing you, little duck!";  

    fn2() {
            console.log("The dragon is", this.something);
    }
}

/* Only mixes one mixin */
class SimpleMixture mixes Mixin1 {

}

/* Mixes several mixins, property definitions in newer mixins (from left
to right) shadow those from previous mixins AND those in the mixture
class itself, including inherited ones. */
class MultiMixture extends Base mixes Mixin1, Mixin2 {

}


var sm1 = new SimpleMixture();
sm1.fn1();

var sm2 = new MultiMixture();
sm2.fn1();
sm2.fn2();

console.log( sm1 instanceof Base );
console.log( sm1 instanceof Mixin1 );
console.log( sm2 instanceof Base );
console.log( sm2 instanceof Mixin2 );

Which emits

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var __applyMixins = this.__applyMixins || function (mixtureTarget) {
    for(var a=1; a<arguments.length; ++a) {
        var mixin = arguments[a];
        Object.getOwnPropertyNames(mixin.prototype).forEach( function(name) {
            mixtureTarget.prototype[name] = mixin.prototype[name];
        })
    }; 
};
var Base = (function () {
    function Base() {
    }
    Base.prototype.say = function (hello) {
        console.log(hello);
    };
    return Base;
})();
var Mixin1 = (function () {
    function Mixin1() {
        this.something = "Couac couac";
    }
    Mixin1.prototype.fn1 = function () {
        console.log(this.something);
    };
    return Mixin1;
})();
var Mixin2 = (function () {
    function Mixin2() {
        this.something = "shadowing you, little duck!";
    }
    Mixin2.prototype.fn2 = function () {
        console.log("The dragon is", this.something);
    };
    return Mixin2;
})();
/* Only mixes one mixin */
var SimpleMixture = (function () {
    function SimpleMixture() {
        Mixin1.apply(this) ;
    }
    __applyMixins(/*mixtureTarget*/ SimpleMixture, Mixin1);
    return SimpleMixture;
})();
/* Mixes several mixins, property definitions in newer mixins (from left
to right) shadow those from previous mixins AND those in the mixture
class itself, including inherited ones. */
var MultiMixture = (function (_super) {
    __extends(MultiMixture, _super);
    function MultiMixture() {
        _super.apply(this, arguments);
        Mixin1.apply(this) ;
        Mixin2.apply(this) ;
    }
    __applyMixins(/*mixtureTarget*/ MultiMixture, Mixin1, Mixin2);
    return MultiMixture;
})(Base);
var sm1 = new SimpleMixture();
sm1.fn1();
var sm2 = new MultiMixture();
sm2.fn1();
sm2.fn2();
console.log(sm1 instanceof Base);
console.log(sm1 instanceof Mixin1);
console.log(sm2 instanceof Base);
console.log(sm2 instanceof Mixin2);

and outputs

>>> Couac couac
>>> shadowing you, little duck!
>>> The dragon is shadowing you, little duck!
>>> false
>>> false
>>> true
>>> false

At this point it seems to work with target ES<6 with quite some limitations (the user can't pass arguments to the mixin constructors). Type checking seems to be working (although I could not make Eclipse or Emacs behave with the new keyword). But be warned Very early experimental preview code from a bad JS programmer who knew nothing about TSC's internals three days ago and who was in a rush. Be warned.

I did this to overcome a maintainability issue with my code and rushed this implementation. It was also an exercise to learn more about TSC. I'm also aware that it might not be a great idea to add keywords to a language each time one gets into trouble, but if others see interest in it or you have suggestions, the branch is here https://github.com/dbarbeau/TypeScript/tree/mixes. Just don't expect this implementation to be stable/complete/anything. Actually, if it can just spark some interest/discussion on how to make mixins more useable, then I'm ok with that.

Daniel
PS: Don't get mad at me :)
PS: I'm now testing this with more "serious code", I will quickly see if it is indeed practical.

@DanielRosenwasser DanielRosenwasser added the Discussion Issues which may not have code impact label Apr 27, 2015
@DanielRosenwasser
Copy link
Member

Hey @dbarbeau, thanks for the heads up on this interesting work! While we're mainly focusing on ES6 work right now, it's interesting to see this what you've done here. Hopefully we'll get some time to sit down, look at some of the changes and, like you said, get an idea of what might be involved if we wanted something like this.

@whitneyit
Copy link
Contributor

This looks very similar to the conversation around traits in #311

@dbarbeau
Copy link
Author

Thanks for the feedback and for pointing out discussion 311. We're going to the same place but they seem to be taking a different way. In both cases there is a new keyword, trait vs mixes (one is a noun, the other a verb). Their proposal is based on a new structure type. My version extends the class declaration. I think my proposal is somewhat more lightweight (and, coming from C++, traits refer to trait classes which are something else) but they do have a lot more background (both academic and with a variety of use cases I didn't even imagine). The comparison is interesting!

@whitneyit
Copy link
Contributor

Yea I agree with the lightweight point. I believe that is the strength and
weakness of mixins over traits. I'll hedge my bets that only one would get
implemented if that.

It's defiantly an interesting topic and in either case I hope that one gets
implemented.

Regards,
James
On 27 Apr 2015 7:19 pm, "dbarbeau" notifications@github.com wrote:

Thanks for the feedback and for pointing out discussion 311. We're going
to the same place but they seem to be taking a different way. In both cases
there is a new keyword, trait vs mixes (one is a noun, the other a verb).
Their proposal is based on a new structure type. My version extends the
class declaration. I think my proposal is somewhat more lightweight (and,
coming from C++, traits refer to trait classes which are something else)
but they do have a lot more background (both academic and with a variety of
use cases I didn't even imagine). The comparison is interesting!


Reply to this email directly or view it on GitHub
#2919 (comment)
.

@dbarbeau
Copy link
Author

The "design decisions" I made were mostly guided by the urge of having something running quickly - which might not be the best way to design a language.

That's why, after contemplating Python's MRO, I went against it : I wouldn't get it right in such a short term. I then figured that conceptually I didn't need anything more than what is described in the handbook, just automate it. Mixins are just that, mixins, not inheritance, MRO would be overkill. Thus the "newer shadows older declaration" approach to conflict solving (conflict smashing would better suit). I couldn't decide if users should be warned about the smashing or not.

That's also why I chose the "mixes" verb, it'd be easier to grep through TSC how "extends" is implemented if I did something similar.

So a bunch of hacks nothing more. The important for me is to have something running. Whatever gets into TS in the end is not important as long as something gets in there to help us mixers! I can then port my code to whatever is chosen.

Regards,
Daniel

PS: I finally got emacs-tss to behave! As easy as a symlink in the end :)

@danquirk
Copy link
Member

Cool stuff. For reference, some other issues related to this concept are #371, #727, #1256

@dbarbeau
Copy link
Author

Thanks @danquirk . There is a lot more to mixins than I originally thought!

For example, this proposal doesn't satisfy @RyanCavanaugh's "after-the-fact mixing" requirement (371), which I clearly had no clue about, I tend to overlook the interfacing to existing JS code.

Issue 311 presents attribute renaming to avoid name conflicts, which allows to do fine mixing instead of brutal shadowing. In the case where the mixins come from one project, chances are that they won't shadow themselves unless it's by design. But when mixing mixins from different origins, attribute renaming can come in handy.

Issue 729 seems to more easily satisfy the "after-the-fact mixing", since it is not in the class declaration but as a decorator function. The | operator is confusing to me because it reuses the union type syntax in a slightly different context. But I does resemble the bitwise | operator in some way. It supports attribute renaming. I think the decorator approach could be great if there was a syntax to also put it close the mixture declaration. In new code, one would put the mixins "inline" rather than after the fact

@mymix // a-la Python, more in-context
class Mixture {
}

Where mymix implements whatever mixing logic is needed. But this is already different from 729 in that mymix would be a high-order function taking an original type and returning a new type (which can shadow the original), not an instance (and from there, the | operator would become useless for mixins). Going down that road, there are no mixin classes but high-order decorators (which can mix classes if the user wants to). How all this is implemented and translates to JS is a mystery, specially in the init of the new class.

Concerning 1256's Intersection Types (thankfully, the Bool alphabet is rather small) I'm trying to grasp the concept but haven't yet found a resource which doesn't satellize me immediately, so I can't comment right now on how this proposal is related to 1256. Sorry.

Daniel

@dbarbeau
Copy link
Author

In this proposal, concerning my issue with calling mixins' constructors, I think I don't need them. The mixins' default constructors are always called with this (right after any super call). this (the mixture) has all the mixed in attributes available in its own constructor and can give values to them.

Ah, I noticed that private mixed-in attribute remain inaccessible to the mixture. I don't think it's desirable : the mixture's state IS exactly the mixin's state (state==members), no?

@jbondc
Copy link
Contributor

jbondc commented Apr 28, 2015

@dbarbeau Nice hacking

You might find this interesting:
https://wiki.php.net/rfc/traits

The Scala example at the bottom was also suggested (with mixin), some interesting history there.

@dbarbeau
Copy link
Author

Thanks @jbondc! That is an interesting read, I hope to get some time to dive deeper into it.

Reading all those links leads me to think that, alright my implementation is lightweight and solved an immediate issue for me (with a slight tweak in the mixin precedence : the mixture's implementation should take precedence over mixins' in my very own case, like they do in the PHP link), but really, it is too lightweight to be future proof.

What may be emerging is a Type algebra, some sort of meta-programming (there's already type union, talk about type intersection...). If that metaprogramming could be taken to the level of a hypothetic Typescript-Type-DSL (that may just look like ES/JS/TS, and can be parsed like it but works on types, just avoid C++ template-style metaprog :) ), you could write things like this (just letting the thoughts machine run, not meant to be realistic):

// Takes type arguments and returns a new type.
// Implements the handbook's mixin algo. Could be part of the stdlib.
function mixin<...Mixins> : <> { // this signature tells the compiler that we're using the type DSL.
    // fun with metaprogramming...
    // Return new type;
}

// For after-the-fact mixing, maybe?
// Takes type arguments and returns a new type.
// Implements the handbook's mixin algo. Could be part of the stdlib.
function post_mixin<Mixture, ...Mixins> : <> {
    // fun with metaprogramming...
    // Return new type;
}

// Takes no type arguments and returns a new type
function custom_static_mixin<> : <> {
    // The user chose to create a new type from a predefined set of classes.
    // Copy properties, rename these, modify flags if you wish because you have a DSL for that.
    // Return new type;
}

/*Since mixin returns a type and extends expects a type, this is valid:*/
class MyClass extends mixin<M1,M2,M3>  {
}

/*Same here, name ambiguity can be resolved by taking the function which returns a type*/
class MyOtherClass extends custom_static_mixin {
} 

/* After the fact? */
class A {
}
A = post_mixin<A, M1, M2, M3>; // mixin and overwrite original type.

In this hypothesis functions convey the idea that the user can use a traditional programming metaphor (e.g. imperative) to customize the output type, instead of a declarative way that may need to extend the language syntax and more lexer/parser effort.

Daniel

@dead-claudia
Copy link

I do feel that an even better idea would be permitting default interface implementations. Java had this very problem, and fixed it with default interface implementations in Java 8. I feel this could profit from the same addition. (I filed #3536 regarding this alternative)

@nippur72
Copy link

Can we replace the verb "mixes" with the word "with" (a la Dart):

class A extends B with M1, M2 { 
}

@dead-claudia
Copy link

I agree that the with seems a little clearer and more descriptive of the intent. Also, it's a little easier to reserve, since with isn't a valid identifier already (and won't need new highlighting for a lot of IDEs).

@benliddicott
Copy link

Well while we are all at it, here is my take. Anything implemented by a class listed further to the left overrides anything implemented by a class further to the right.

"use strict";
/**
 * Mixins for multiple inheritance. 
 * Usage:
 * class Derived extends BaseOne implements BaseTwo, BaseThree{
 *      baseTwoFunction: (a:number)=>any;
 *      baseThreeProperty: string;
 * 
 *      // Apply any missing base class functions to the derived prototype
 *      private static _mixins = Mixins.Apply(Derived, [BaseTwo, BaseThree]);
 * }
 */
module Mixins {
    export function Apply(derivedClass: any, baseClasses: any[]) {
        var ret: { baseClass: any, functions: { name: string; done: boolean; type: string; }[] }[];
        ret = [];
        baseClasses.forEach(baseClass=> {
            ret.push({ baseClass: baseClass, functions: [] });
            Object.getOwnPropertyNames(baseClass.prototype).forEach(name=> {
                // ONLY if doesn't already exist.
                // Is this a property descriptor on the base class prototype?
                var basePD = Object.getOwnPropertyDescriptor(baseClass.prototype, name);
                var derivedPD = Object.getOwnPropertyDescriptor(derivedClass.prototype, name);
                if (basePD && !derivedPD) {
                    Object.defineProperty(derivedClass.prototype, name, basePD);
                    ret[ret.length - 1].functions.push({
                        name: name, done: true, type: (basePD.get && basePD.set ? "get set" : basePD.get ? "get" : basePD.set ? "set" : basePD.value ? "method" : "")
                    });
                } else {
                    ret[ret.length - 1].functions.push({
                        name: name, done: false, type: (basePD.get && basePD.set ? "get set" : basePD.get ? "get" : basePD.set ? "set" : basePD.value ? "method" : "")
                    });
                }
            });
        });
        return ret;
    }
} 

Further thoughts:

It would be good for mixin-aware mixins to be able to be called in the super() call. I.e. they could provide a method mixin or init or something, to be called as BaseTwo.prototype.init.call(this). These would be called in right-to-left order.

Note also that currently mixins cannot have protected or private members. See #3854 for a proposal on how to fix this.

@dead-claudia
Copy link

This and generators are the only real blocking things from me being able to use TypeScript in my project instead of Babel. (I need the generators for laziness, and mixins are used extensively in one of my files, where I would otherwise need a deep multiple-inheritance hierarchy, because of my large number of blurred is-a/has-a relationships in it, most of which need checked at runtime, and so they have to be added as instance properties.

@benliddicott
Copy link

Is the need for language support met by Intersection Types #3622 (committed)? See also "Feature: type alternatives (to facilitate traits/mixins/type decorators)" #727, and "optional interface" #371 aiming at the same target.

Syntax: A & B is a type which implements both A and B. (In other words roughly equivalent to interface A_n_B implements A, B{}) It can be used wherever union types can be used.

Synopsis:

interface Emitter{ 
  on(event, fn);
  once(event, fn);
  off(event, fn);
  emit(event);
}

function MakeEmitter<T>(o T: ( T & Emitter) {
   require('my-emitter-library').mixin(o);
   return <T & Emitter>o;
}

Or you could provide a .d.ts which typed the function mixin as returning the appropriate intersection type.

@RichiCoder1
Copy link

Intersection Types meets the needs of the Typing, but not the emit.

@benliddicott
Copy link

@RichiCoder1 , the emit is a single line for each class. I'm using mixins all day long using the Mixin module I posted, and I don't think it's a big deal. Does the emit really, really, really need language support?

@kitsonk
Copy link
Contributor

kitsonk commented Sep 23, 2015

The rest operator for Generics is covered under #3870

@jbondc
Copy link
Contributor

jbondc commented Sep 23, 2015

Thanks @kitsonk

@jklmli
Copy link

jklmli commented Nov 17, 2015

A syntax that closely aligns with the new intersection types would be something like:

class Test extends B & C & D {
  ...
}

@falsandtru
Copy link
Contributor

+1 for a with/mixes keyword and constructor restrictions.

@JoshMcCullough
Copy link

So, really ... multiple inheritance? Would be cool. Plus partials (#563).

@pleerock
Copy link

+1 for mixins

@dbarbeau
Copy link
Author

dbarbeau commented Jan 9, 2016

Hello,

Well, for what it's worth, I'm trying to rebase my branch on master. Asides merging issues, I'm hitting some errors relative to getApparentType which returns a type where the mixed in members do not exist. The code has evolved quite a bit and I can't wrap my head around the bits involved in the bug. Well, at this point I'm stuck and spent the time I could afford to spend on the rebase. I will retry later unless the discussion finally focuses on some other solution.

This hack came in handy for quite a few months but after several refactorings, my work doesn't rely on mixins anymore. I thus have less interest in rebasing too.

To be continued.

@Izhaki
Copy link

Izhaki commented Jan 15, 2016

@DanielRosenwasser

There seems to be a recurring pattern here, which is unfair, annoying, and actually counter healthy programming practices.

This is exactly the same issue I've complaint about with regards to function overloading.

What's a framework

Typescript is a superset of Javascript. It is a framework in the sense that it takes common behaviour requirements and generalises them. For example, we can now write the class keyword instead of using function constructors and proprietary code to handle inheritance et al.

Now, if what you have to do to achieve a behaviour is write bespoke code, partly by using code provided code snippets - that's not the definition of something that a framework support.

As such, currently typescript has neither support for mixins, nor for function overloading. In the case of the former, we still need to copy the interface, and add some extra code; in the case of the latter, we still need to implement a router per case.

While this is fine and understood from prioritisation perspective, it is unfair that the documentation (handbook) claims such support.

If by providing examples of how to achieve something that is not generalised by typescript you consider that something to be a feature of typescript, you can just equally claim to support anything imaginable - like animation of social networks.

The problem

Consider I have two classes in already class hierarchy -svgViewee and canvasViewee, both has some common behaviour that is defined in a coreViewee class (which they cannot inherent from as they are already part of a class hierarchy).

To mixin the coreViewee into the two others, one needs to follow the steps described in the handbook

Now say the interface of Viewee changes. One has to update the interface in 3 separate locations. Definitely not a generalisation.

True, in the way things are done now, at least no action is needed when the implementation changes. But still, is this really what mixins are about? Absolutely not.

What makes matters worse, is that instead of the proposed solution, one is much better just using some pre-compilation method to pull in a source file including both the interface and implementation into the relevant classes. So a change is always needed in one place only.

The current solution for mixins promotes code duplication and thus potential inconsistency. And all this, in my view, is in order to falsely claim typescript supports mixins.

Suggestions

I honestly think that you should move mixins and function overloading into a section called 'workarounds' or something. You simply can't claim to support these features.

No body reads the whole handbook before choosing to opt for typescript. You check if it supports X, Y, Z and make a decision. So it appears by the handbook that typescript supports mixins and function overloading, where in practice it doesn't.

@kitsonk
Copy link
Contributor

kitsonk commented Jan 16, 2016

And all this, in my view, is in order to falsely claim typescript supports mixins.

Those a pretty serious accusations... as if the team are trying to "trick" you into choosing TypeScript. I doubt they set out, to trick everyone into using TypeScript on the back of mixins. The handbook gives an example of how to support that pattern in TypeScript.

The current situation is that the TypeScript team are very very cautious about introducing functionality that is likely to conflict with something that TC39 has indicated that they will look at at some point in the future. There have been several things that the TypeScript team would take back, considering where TC39 ended up (e.g. modules). I have personally chatted with a member of TC39 and they indicated that native "mixin" or "composition" type support for classes is that something that was part of the original class proposal, but got too heated and too contended and so it got pulled out, to be revisited at a future date. TC39 has made it abundantly clear this is an area that they will address at some point, though I am personally disappointed that it doesn't seem to be at the forefront of any of the members agendas at this moment.

My understanding (based on comments by @RyanCavanaugh) is that the team would consider something if they think it is far enough in the future, that TypeScript has a vested interest in that domain and that the "TypeScript solution" could win as the final syntax in TC39. I don't see that happening with mixins.

We (Dojo) like mixins and composition and we like TypeScript. We were able to get something acceptable, with a level of type inference out of TypeScript that addresses some of the concerns you point out about about. While it isn't straight forward and simple, it is possible to get to a point where it is simple for the end developer.

@Izhaki
Copy link

Izhaki commented Jan 20, 2016

Perhaps it is worth directing people to #6502, where @jods4 proposes a mixin solution that I suspect some will find better than the current handbook proposal:

function Mixin<T>(...mixins) : new() => T {
    class X {
        constructor() {
            mixins[0].call(this);
        }
    }
    Object.assign(X.prototype, ...mixins.map(m => m.prototype));
    return <any>X;
}

abstract class Dog {
    bark() { console.log('bark!'); }
}

abstract class Duck {
    quack() { console.log('quack!'); }
}

class Base {
    constructor() {
        console.log('base ctor');
    }

    world = "world";

    hello() {
        console.log(`hello ${this.world}`);
    }
}

interface DoggyDuck extends Base, Dog, Duck { }

class Mutant extends Mixin<DoggyDuck>(Base, Dog, Duck) {

}

let xavier = new Mutant();
xavier.bark();
xavier.quack();
xavier.hello();

@jklmli
Copy link

jklmli commented Feb 18, 2016

@Izhaki

This doesn't seem to work if Base extends another class.

@jods4
Copy link

jods4 commented Feb 19, 2016

@jiaweihli I made up that sample code to show how we could make TS typing work.
The mixin code is not top notch but can easily be fixed, try this:

function Mixin<T>(base, ...mixins) : new() => T {
    X.prototype = new base();
    X.prototype.constructor = X;
    function X() {}
    Object.assign(X.prototype, ...mixins.map(m => m.prototype));    
    return <any>X;
}

abstract class Dog {
    bark() { console.log('bark!'); }
}

abstract class Duck {
    quack() { console.log('quack!'); }
}

class BaseBase {
    constructor() {
        console.log('base base ctor');
    }   

    ping() {
        console.log('pong');
    }
}

class Base extends BaseBase {
    constructor() {
        super();
        console.log('base ctor');
    }

    world = "world";

    hello() {
        console.log(`hello ${this.world}`);
    }
}

interface DoggyDuck extends Base, Dog, Duck { }

class Mutant extends Mixin<DoggyDuck>(Base, Dog, Duck) {

}

let xavier = new Mutant();
xavier.bark();
xavier.quack();
xavier.hello();
xavier.ping();

@jklmli
Copy link

jklmli commented Feb 22, 2016

@jods4
Thanks for the updated suggestion - the only issue I have is that I have no way of enforcing type pre-requisites in mixins. (i.e. I can't declare self-types).

For a concrete example of when this is a problem: consider the case where I want to create a new class D based on class B and trait C, where class B extends A and C needs A as a prereq. Without self-types and naively mixing in B then C, I end up creating a new class with a linearized type order of A, B, A, C. This means B's changes over class A can potentially be lost. (additions are safe though)


In the end, I decided to go with MDN's approach, which works seamlessly with TypeScript though it's a little bit more verbose versus having direct language support.

I think the MDN approach should be the recommended approach over what's currently shown in the handbook, which was written when things like local classes weren't yet supported. Thoughts @DanielRosenwasser ?

@jods4
Copy link

jods4 commented Feb 23, 2016

@jiaweihli I'm not familiar with those concepts so I won't be able to help much here.
To start with, I am unsure what the self-type pattern actually means applied to JS, as all references I could find were about Scala (and I don't have much experience with Scala).

I found one thing weird, though. You said "C needs A as a prereq", which I understand is what self-types is about and is explicitely different from "C is a A". But then you said that mixing B, C would result in linearized type order of A, B, A, C. What would be introducing the second A? My poor understanding of self-types is that C itself is not a A so it wouldn't apply A members again. It merely requires that B is already a A, which would be fine in your example (although there seems to be no way to enforce that in TS).

MDN approach looks good btw if it works properly in TS. There's not much verbosity, maybe a little in the combination of the mixins. Consider that the toy implementation above currently requires an interface for each combination of mixins, which is not ideal either. Only drawback I see is that lots of intermediate classes are created if you compose many mixins. Probably not a problem, though.

@jklmli
Copy link

jklmli commented Feb 23, 2016

@jods4
The example I gave was to illustrate a roadblock I ran into (actually earlier today 😛) with the approach you outlined earlier, which didn't have a straightforward way of representing self-types. Naively writing abstract class C extends A won't work because it'll mix in A again.

You're correct in your analysis that this isn't an issue with self-types, and that a correct ordering of A, B, C will result. The MDN approach supports this.

@unional
Copy link
Contributor

unional commented Mar 9, 2016

@jiaweihli , How do you implement the MDN's approach in TypeScript? I'm interested to learn. Do you have an example?

@jklmli
Copy link

jklmli commented Mar 11, 2016

@unional Here's what I'm using right now:

const Dog =
  // type prerequisite: can only mixin to classes derived from Base,
  // but won't mixin Base again.
  (baseClass: { new(...args: Array<any>): Base }) => {
    return class extends baseClass {
      bark(): void { console.log('bark'); }
    };
  };

const Duck =
  (baseClass: { new(...args: Array<any>): Base }) => {
    return class extends baseClass {
      quack(): void { console.log('quack'); }
    };
  };

class BaseBase {
  constructor() {
    console.log('base base ctor');
  }

  ping() {
    console.log('pong');
  }
}

class Base extends BaseBase {
  constructor() {
    super();
    console.log('base ctor');
  }

  world = "world";

  hello() {
    console.log(`hello ${this.world}`);
  }
}

class Mutant extends Dog(Duck(Base)) {}

let xavier = new Mutant();
xavier.bark();
// :KLUDGE: Type inferencing doesn't work for any mixins after the first.  
// But the compiled code is correct.
xavier['quack']();
xavier.hello();
xavier.ping();

@jklmli
Copy link

jklmli commented Mar 11, 2016

@DanielRosenwasser re: my code above ^
It looks like dynamic anonymous classes don't have full type inferencing. If I manually declare:

const Quacker = class extends Base {
  quack(): void { console.log('quack'); }
};

const BarkQuacker = class extends Quacker {
  bark(): void { console.log('bark'); }
};

(new BarkQuacker).quack();

then things work as expected.

Is there any chance of improving the above issue? If this is fixed, then traits can work with no additional effort.

@jods4
Copy link

jods4 commented Mar 12, 2016

@jiaweihli The :KLUDGE: is a major drawback, you loose all typing for all but one mixin...

The problem is that statically, Dog returns Base & { bark(): void } (more or less). That's why you loose any other type info.

Dynamic type info is not possible (by definition) but the closest thing is generics.

Unfortunately, when I try

function Dog<T extends Base>(baseClass: { new(...args: Array<any>): T }) => {
    return class extends baseClass {
      bark(): void { console.log('bark'); }
    };
  };

It does not compile because TS does not recognize baseClass as a class anymore.

At its hearth, this is the same limitation that requires the supplementary interface in my code above.

@jklmli
Copy link

jklmli commented Mar 12, 2016

@jods4

Yeah, I didn't realize this issue until today when I tried to apply the approach to multiple mixins 😞.
Luckily for me, I only need a single mixin for the problem I'm trying to solve (but I do need to avoid overwrites from repeated inherited types).

The multiple mixins compose correctly from the compiled code, but it seems like some extra work is needed to fix the type inferencing in the compiler.

@dead-claudia
Copy link

Edit: fix misplaced variable

Wonder if this could be solved with rest types?

interface MixinType1<T extends Object, ...U extends Object[]>
    extends T & MixinType<...U> {}

type MixinType<T extends Object, ...U extends Object[]> = T & MixinType1<...U>

function mixin<T extends Object, ...U extends Object[]>(host: MixinType<T, ...U>, ...objects: ...U)
function mixin<
    T extends Object & {[key: string]: U},
    U,
    ...V extends Array<Object | {[key: string]: U>
>(host: MixinType<T, ...V>, ...objects: ...V)

function mixin(host: {[key: string]: any}, ...args: {[key: string]: any}[]) {
    for (const arg of args) {
        for (const key of Object.keys(arg)) {
            (<any> host)[key] = (<any> arg)[key]
        }
    }

    return host
}

@falsandtru
Copy link
Contributor

My polyfill:

export function Mixin<T>(...mixins: Array<new (...args: any[]) => any>): new (...args: any[]) => T {
  return mixins.reduceRight((b, d) => __extends(d, b), class { });
}

function __extends(d: new () => any, b: new () => any): new () => any {
  const __ = class {
    constructor() {
      return d.apply(b.apply(this, arguments) || this, arguments);
    }
  };
  void Object.assign(__.prototype, d.prototype, b.prototype);
  for (const p in b) if (b.hasOwnProperty(p)) __[p] = b[p];
  for (const p in d) if (d.hasOwnProperty(p)) __[p] = d[p];
  return __;
}
    it('2', () => {
      let cnt = 0;
      class A {
        constructor(n: number) {
          assert(++cnt === 1);
          assert(n === 0);
        }
        static a = 'A';
        ap = 'a';
        am() {
          return this.ap;
        }
      }
      class B {
        constructor(n: number) {
          assert(++cnt === 2);
          assert(n === 0);
        }
        static b = 'B';
        bp = 'b';
        bm() {
          return this.bp;
        }
      }
      interface AB extends B, A {
      }
      class X extends Mixin<AB>(B, A) {
        constructor(n: number) {
          super(n);
          assert(++cnt === 3);
          assert(n === 0);
        }
        static x = 'X';
        xp = 'x';
        xm() {
          return this.xp;
        }
      }
      const x = new X(0);
      assert(++cnt === 4);
      assert(x instanceof A === false);
      assert(x instanceof B === false);
      assert(x instanceof X);
      assert(X['a'] === 'A');
      assert(X['b'] === 'B');
      assert(X.x === 'X');
      assert(x.ap === 'a');
      assert(x.bp === 'b');
      assert(x.xp === 'x');
      assert(x.am() === 'a');
      assert(x.bm() === 'b');
      assert(x.xm() === 'x');
    });

@kitsonk
Copy link
Contributor

kitsonk commented Jun 12, 2016

@falsandtru thanks for sharing. Of course a polyfill generally is a term to provide functionality for some standard that exists when not natively supported. I am unaware of any standard that currently exists in ECMAScript (partly why the TypeScript have been loathe to address it).

Your solution, like a lot of other solutions, would benefit from the rest operator for generics, but even then there are still some problems with all of this. While not a problem, it is a significant amount of boilerplate to create the transitory interface AB. Also, there is currently no way to model in the types what your actual _extends does. While the particular example you give doesn't have any property conflicts, it quickly falls down when you do and your typing can quite easily not represent the actual code. For example:

class A {
  a: string;
}

class B {
  a(): string {
    return 'a'
  }
}

interface AB extends B, A { } // Error cannot simultaneously extend

type ABType = B & A; // Can't be used with Mixin, but is type `string & () => string`

class C extends Mixin<AB>(A, B) {
    c: number;
}

This are similar problems we have run into with dojo-compose although we have been able to get a level of type inference working where in many use cases, the user doesn't have to do any boilerplate, thought our method resolution, like the __extends you provided cannot be modelled, so it requires the user to know this and provide an final interface that represents it accurately.

This is why I hope, at some point, we are provided with a way to programatically modify the types, either through ambient decorators or more feature rich type operators.

@dead-claudia
Copy link

dead-claudia commented Jun 17, 2016

Based on what I'm starting to see in ES developments, I highly suspect that mixins will end up as simple decorators, and if it ever becomes part of the language, it would just end up a new builtin function. You could do it this way in ES + decorators:

const wm = new WeakMap()

export function mixin(Cls) {
  class C extends Cls {
    static [Symbol.hasInstance](obj) {
      return Array.from(wm.get(C)).some(C => obj instanceof C)
    }
  }

  Object.defineProperty(C, "name",
    Object.getOwnPropertyDescriptor(Cls, "name"))

  wm.set(C, new Set())
  return C
}

export function mixes(...mixins) {
  return Cls => {
    for (const m of mixins) {
      if (!wm.has(m)) {
        throw new TypeError(`Cannot add non-mixin ${m.name} as a mixin`)
      }

      wm.get(m).add(Cls)
    }

    for (const proto of mixins.map(m => Object.getPrototypeOf(m.prototype)) {
      Object.getOwnPropertyNames(proto)
      .filter(prop => prop !== "constructor")
      .forEach(prop => {
        Object.defineProperty(Cls.prototype, prop,
          Object.getOwnPropertyDescriptor(proto, prop))
      })
    }
  }
}

If anything, the only way you can realistically get this kind of mixin in TypeScript is by supporting variadic type intersection (i.e. A & B & ...) somehow. And that requires variadic types first, which don't yet exist.

@shlomiassaf
Copy link

@kitsonk, note that @falsandtru 's solution will throw an error when the compilation target is ES6 and up.

You can not treat a class as a function in ES6.

Invoking a class constructor results in the following error:

TypeError: Class constructor XXXXXX cannot be invoked without 'new'

@ahejlsberg
Copy link
Member

Closing as mixin classes are now supported in #13743.

@hoegge
Copy link

hoegge commented Jul 13, 2017

Couldn't TypesScript extend just implement the described "workaround" in https://www.typescriptlang.org/docs/handbook/mixins.html so that it does like that when you write:

class DaughterClass extends MotherClass, FatherClass, MailManClass {
    // additional implementation + constructor implementation that calls all super constructors
   constructor(public value: any) {
       super<Motherclass>.constructor(value);
       super<FatherClass>.constructor(value);
       super<MailManClass>.constructor(value);
       doSomethingMore(value);
    }

   niceCollidingMethod(aParameter: any) {
        super<MotherClass>.niceCollidingMethod(aParameter);
        aCommandToExecuteInBetween(aParameter);
        super<FatherClass>.niceCollidingMethod(aParameter);
        return finalStuffToDo(aParameter);
}

and then a requirement to re-implement methods or members that collide (diamond problem) which then has to call the super methods in addition to additional actions. Not sure if the suggested super<ancestor>.function notation would make sense / be accepted

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests