Partial implementation of an interface using mixins

Topics: General, Language Specification
Dec 11, 2012 at 9:11 PM
Edited Dec 13, 2012 at 9:43 AM

Hi,

We'd like to see support for mixins in TypeScript (as also requested by a few others). Taking an example from the web, here is a working implementation that should be consistent with TypeScript principles (properties on the instance, methods on the prototype):

 

 

// Compiler generated function for copying methods or properties
// from source to destination
function extend(destination, source, copyMethodsOnly?) {
    for (var name in source) {
        if (!destination.hasOwnProperty(name)) {
            if (copyMethodsOnly) {
                if (typeof source[name] === "function") {
                    destination[name] = source[name];
                }
            }
            else {
                if (typeof source[name] !== "function") {
                    destination[name] = source[name];
                }
            }
        }
    }
}

// An interface that we would like to implement partially
interface IRectangle {
    area(): string;
    grow(): void;
}

// The mixin type
class Rectangle implements IRectangle {

    public height: number;
    public width: number;

    constructor(private label: string) {
    }

    public area() {
        return this.label + (this.height * this.width);
    }

    public grow() {
        this.height++; this.width++;
    }
}

// The sample type demonstrating usage of the mixin
// Block comments denote working code, and should be removed once the "mixin" keyword has been introduced
class FixedHeightRect /*implements IRectangle*/ {

    // Declare the mixin using the keyword
    private /*mixin*/ rectangle = new Rectangle(label);

    constructor(label: string, private width: number, defaultHeight: number = 5) {

        // Compiler generated function call for copying properties from the mixin to the sample instance
        // We shouldn't need to write this line of code
        extend(this, new Rectangle(label)); // Mixin type permits constructor arguments

        this["height"] = defaultHeight; // We won't write this, rather we will write the next line
        /* this.rectangle.height = defaultHeight; */ // The compiler replaces all references to "this.rectangle" with "this"
    }

    // Permit overriding of a mixin method
    public grow() {
        this.width++;
    }
}

// Compiler generated, one-off function call for copying methods from the mixin prototype to the sample prototype;
// We shouldn't need to write this line of code
extend(FixedHeightRect.prototype, Rectangle.prototype, true);


// Sample usage
var rect = new FixedHeightRect("Area = ", 10);
rect.grow(); // Use the overridden grow; the height will not change
var area = rect["area"](); // Use the mixin function

alert(area); // Alerts "Area = 55"

 

-----------------------------------------------------------------------------------------------------------

 

Hopefully the comments are clear enough. Basically, we would like the type FixedHeightRect implement the IRectangle interface, but do so by only implementing a subset of the interface, while mixing-in the default implementation from Rectangle.

Clearly we could have FixedHeightRect inherit from Rectangle, but I'd like to avoid steering the discussion into the inheritance versus composition debate, but rather consider the benefits of having mixins supported natively by the language.

The code above should work in its present form. The suggestion is that TypeScript permit the rectangle property to be declared with the "mixin" keyword (or something similar)  that would magically endow the FixedHeightRect with the properties and methods of Rectangle.

Any thoughts?

Thanks

PS: The extend would need to recurse up the prototype chain - omitted for clarity.

Coordinator
Dec 13, 2012 at 7:35 PM

Thanks for taking the time to put your idea down.

We've seen some thoughts on the forums around mixins, and I agree they could be very handy in cases like you've described.  For the near future, we'll likely focus on just getting a 1.0 out the door, and that's largely going to be trying to make it easier to use and type JavaScript as well as align with ES6 as the standard is completed.  After 1.0, we'll likely then start looking at features, like mixins, that really help the language be richer and more expressive.

Dec 13, 2012 at 9:54 PM
Edited Dec 13, 2012 at 10:01 PM

nabog, while your example does work you have an issue currently that you won't get any intelisense for your mixed in methods.  With a slight change to your mixin interface you can fix this:

// Compiler generated function for copying methods or properties
// from source to destination
function extend(destination, source, copyMethodsOnly?) {
    for (var name in source) {
        if (!destination.hasOwnProperty(name)) {
            if (copyMethodsOnly) {
                if (typeof source[name] === "function") {
                    destination[name] = source[name];
                }
            }
            else {
                if (typeof source[name] !== "function") {
                    destination[name] = source[name];
                }
            }
        }
    }
}

// An interface that we would like to implement partially
interface IRectangle {
    area: () => string;
    grow: () => void;
}

// The mixin type
class Rectangle implements IRectangle {

    public height: number;
    public width: number;

    constructor(private label: string) {
    }

    public area() {
        return this.label + (this.height * this.width);
    }

    public grow() {
        this.height++; this.width++;
    }
}

// The sample type demonstrating usage of the mixin
// Block comments denote working code, and should be removed once the "mixin" keyword has been introduced
class FixedHeightRect implements IRectangle {

	// Property implemented by mixin
	private height;
	
    constructor(label: string, private width: number, defaultHeight: number = 5) {

        // Compiler generated function call for copying properties from the mixin to the sample instance
        // We shouldn't need to write this line of code
        extend(this, new Rectangle(label)); // Mixin type permits constructor arguments

        this.height = defaultHeight; // We won't write this, rather we will write the next line
        /* this.rectangle.height = defaultHeight; */ // The compiler replaces all references to "this.rectangle" with "this"
    }

	// IRectange members
    public area: () => string;
    public grow() {
        this.width++;
    }
}

// Compiler generated, one-off function call for copying methods from the mixin prototype to the sample prototype;
// We shouldn't need to write this line of code
extend(FixedHeightRect.prototype, Rectangle.prototype, true);


// Sample usage
var rect = new FixedHeightRect("Area = ", 10);
rect.grow(); // Use the overridden grow; the height will not change
var area = rect.area(); // Use the mixin function

alert(area); // Alerts "Area = 55"

 

 If you change your mixin defined methods to be properties instead of functions you can then say that you FixedHeightRect class implements IRectangle so you'll get intelisense for these methods.  While it's true that you then need to say your class has all of these properties, you only need to give an implementation for the ones you want to override. 

Dec 14, 2012 at 6:49 PM

@jonturner,

I'm glad you agree this will be a handy feature. No need to thank me for putting ideas down: the fact is it is TypeScript that has made these ideas possible. The overlaying of a type system over the inherently dynamic JavaScript language suggests many interesting possibilities.

 

@ickman,

Yes, that is a useful interim solution. However, that is actually also the feature that I'm suggesting. That the TypeScript compiler add the properties and methods on your behalf, so that you can explicitly declare the IRectangle interface on the FixedHeightRect type, leading to resolution of statements such as "var area = rect.area();", without the need for the bracket syntax.

The problem with declaring the properties and methods yourself is that it creates rather a significant maintenance problem. Imagine that five different types implement IRectangle. Now suppose we want to add a new method to the interface, say, "changeColour(colour: string)". Clearly this will break all five types and Rectangle as well. If on the other hand the compiler does the work for you, then all you have to do is implement the new method on Rectangle, and hey presto!

Noel