Supporting mixins and c# like attributes

Topics: General
Oct 7, 2012 at 7:44 PM
Edited Oct 9, 2012 at 2:02 AM

There's been a bit of talk on the forums around supporting mixins in TypeScript and with the 0.8 drop you have to use interfaces to achieve this.  I wanted to see if I could streamline the process of defining mixins a bit and what I ended up creating is a base class that add support for not only defining mixins but also c# like attributes.  Copy the code below into the playground and run it to see that it works:

update: see the simplified approach to mixins that I posted a few messages down.

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                var _attribute = _constructor.__attributes__[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }

    // Static methods

    static attribute(type: new () => ObjectBase, attribute: IAttribute): void {
        if (!(<any>type).__attributes__) {
            (<any>type).__attributes__ = [];
        }
        (<any>type).__attributes__.push(attribute);
    }

    static hasAttribute(obj: any, attribute: new () => IAttribute): bool {
        var _constructor = (<any>obj).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                if (_constructor.__attributes__[i] instanceof attribute) {
                    return true;
                }
            }
        }
        return false;
    }

    static mixin(type: new () => ObjectBase, mixin: IMixin): void {
        mixin.extend(type.prototype);
        if (mixin.onCreated) {
            attribute(type, mixin);
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

interface IMixin extends IAttribute {
    extend(prototype: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }

    static addTo(type: new () => ObjectBase, prefix = 'cb_'): void {
        if (!(prefix in cache)) {
            cache[prefix] = new CallbacksAttribute(prefix);
        }
        ObjectBase.attribute(type, cache[prefix]);
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose (): void;
	isDisposed (): bool;
}

class DisposableMixin implements IMixin {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

    static addTo(type: new () => ObjectBase): void {
        if (!mixin) {
            mixin = new DisposableMixin();
        }
        ObjectBase.mixin(type, mixin);
    }
	static mixin: DisposableMixin = null; 
}


// Example attribute usage

class Foo extends ObjectBase {
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
CallbacksAttribute.addTo(Foo);

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase {
	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
	
	static create(): IBar {
		return <any> new Bar();
	}
}
DisposableMixin.addTo(Bar);

// With mixins you need to use interfaces to represent your class
interface IBar extends IDisposable {
	test();
}

var bar = Bar.create();
bar.dispose();

Oct 7, 2012 at 7:55 PM
Edited Oct 7, 2012 at 7:57 PM

Breaking down how the code above works... Mixins & attributes get hung off your class as a static array of objects that will be walked anytime a new instance of your class is created.  If the attribute/mixin implements an onCreated() method it will be called with the instance of the newly created object.  This gives your attribute/mixin an opportunity to do something to the newly created object.  In the CallbacksAttribute what I'm doing is walking the objects methods and binding any method starting with 'cb_' to its 'this' pointer.  Basically you can't call that method accidentally in any other context which is ideal for callbacks. 

For mixins there's an added extend() method.  When you add the mixin to a class the extend() method is called with the classes prototype so that the additional methods can be added to the class.  What's nice about this approach over other mixin system is that we can use TypeScript to strongly type the prototype and we can validate that we're wiring up the right set of extensons.  At create time the mixin acts like an attribute and the mixin can optionally register an onCreated() method that should be called anytime a new instance of the class is created.  This essentially lets the mixin hook into the constructor for every class its attached to. 

Oct 8, 2012 at 8:17 PM

All we need is language level support for this and to have this info available in the AST also, not just runtime. I opened a discussion about this here.

Oct 9, 2012 at 2:01 AM

For mixins, I wasn't crazy about the need to redefine your class as a seperate interface that you then had to pass around everywhere, so I've come up with an alternative approach to describing mixins.  It's still kind of crappy but now slightly less crappy.

In the example below everything is basically the same as my original code but I've changed the may I describe the interfaces for a mixin.  No I'm describing my mixin as all properties, even the methods.  This lets you update your class to say it implements the mixin interface and while you need to add the definitions for all of your mixins properties to your class you don't actually need to provide any implementation.  From TypeScripts perspective, it's fine with the value of these properties being set externally (which is what happens when you add in the mixin) and if you look at the generated code you see that all of these additional property declerations boil out so they have no added overhead.

It sucks that you have to manually declare all of your mixins methods as properties of your class but all things considered this adds zero added overhead to the generated output and seems to be the simplest approach to supporting mixins in the current drop of TypeScript.

 

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                var _attribute = _constructor.__attributes__[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }

    // Static methods

    static attribute(type: new () => ObjectBase, attribute: IAttribute): void {
        if (!(<any>type).__attributes__) {
            (<any>type).__attributes__ = [];
        }
        (<any>type).__attributes__.push(attribute);
    }

    static hasAttribute(obj: any, attribute: new () => IAttribute): bool {
        var _constructor = (<any>obj).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                if (_constructor.__attributes__[i] instanceof attribute) {
                    return true;
                }
            }
        }
        return false;
    }

    static mixin(type: new () => ObjectBase, mixin: IMixin): void {
        mixin.extend(type.prototype);
        if (mixin.onCreated) {
            attribute(type, mixin);
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

interface IMixin extends IAttribute {
    extend(prototype: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }

    static addTo(type: new () => ObjectBase, prefix = 'cb_'): void {
        if (!(prefix in cache)) {
            cache[prefix] = new CallbacksAttribute(prefix);
        }
        ObjectBase.attribute(type, cache[prefix]);
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose: ()=> void;
	isDisposed: () => bool;
}

class DisposableMixin implements IMixin {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

    static addTo(type: new () => ObjectBase): void {
        if (!mixin) {
            mixin = new DisposableMixin();
        }
        ObjectBase.mixin(type, mixin);
    }
	static mixin: DisposableMixin = null; 
}


// Example attribute usage

class Foo extends ObjectBase {
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
CallbacksAttribute.addTo(Foo);

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase implements IDisposable {
	// IDisposable mixin
	public dispose: () => void;
	public isDisposed: () => bool;

	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
}
DisposableMixin.addTo(Bar);

var bar = new Bar();
bar.dispose();

 

Oct 9, 2012 at 4:25 AM

Method syntax and property syntax are equivalent in TypeScript. You can replace methods as easily as you can properties. The advantage of property syntax is that you don't need to fake up an implementation and it's obvious that you haven't supplied one when you look at the code, but you don't have to use property syntax if you don't want to.

class methodsCanBeReplaced {
    method() { }
}

methodsCanBeReplaced.prototype.method = () => alert('blah');

var proof = new methodsCanBeReplaced();

proof.method();



Oct 9, 2012 at 5:54 PM
Edited Oct 9, 2012 at 5:54 PM

Yes, either one can be overriden but I was recommending the use of properties because they don't result in any additional JavaScript being generated.  If you use methods you'll have to stub them out with default implementations which will bloat your output code.

Oct 10, 2012 at 3:20 PM

By slightly modifying the addTo methods to return the attribute, we could have the attributes actually specified within the class, which feels cleaner. This does expose the __attributes__ field, though.

class Foo extends ObjectBase {
	static __attributes__ = [ CallbacksAttribute.addTo(Foo) ];
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
Oct 10, 2012 at 6:44 PM

If you're going to do that I'd probably do some other simplifications.  I'd call the field 'attributes' instead of '__attributes__', then I'd apply the attribute as a function call, and then eliminate the helper routines from ObjectBase. Here's the revised example:

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.attributes) {
            for (var i = 0, l = _constructor.attributes.length; i < l; i++) {
                var _attribute = _constructor.attributes[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

function callbacksAttribute(type: new () => ObjectBase, prefix = 'cb_'): IAttribute {
	var cache = CallbacksAttribute.cache;
    if (!(prefix in cache)) {
        cache[prefix] = new CallbacksAttribute(prefix);
    }
	return cache[prefix];
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose: ()=> void;
	isDisposed: () => bool;
}

class DisposableMixin implements IAttribute {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

	static cache: DisposableMixin = null; 
}

function disposableMixin(type: new () => ObjectBase): IAttribute {
	var mixin = DisposableMixin.cache;
    if (!mixin) {
        mixin = DisposableMixin.cache = new DisposableMixin();
    }
	mixin.extend(type.prototype);
	return mixin;
}

// Example attribute usage

class Foo extends ObjectBase {
	static attributes = [callbacksAttribute(Foo)];

	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase implements IDisposable {
	static attributes = [disposableMixin(Bar)];
	
	// IDisposable mixin
	public dispose: () => void;
	public isDisposed: () => bool;

	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
}

var bar = new Bar();
bar.dispose();

I like the idea of the attributes being defined at the top of the class but it does come at a cost.  If you look at the generated code for Bar you'll see that your attributes are executed before Bar's methods are added to its prototypes.  That's probably ok but it means the attribute can't reason over the class definition at parse time and instead would have to defer any of that logic to runtime on first create.  Again, that's probably ok.

var Bar = (function (_super) {
    __extends(Bar, _super);
    function Bar() {
        _super.apply(this, arguments);

    }
    Bar.attributes = [
        disposableMixin(Bar)
    ];
    Bar.prototype.test = function () {
    };
    Bar.prototype.onDispose = function () {
        alert('object disposed');
    };
    return Bar;
})(ObjectBase);
Dec 9, 2012 at 3:44 PM

Isn't it a problem that the solution requires a specific base class?

I mean, in practice you could just define the mixins on the base class and be done with it. Am I missing something?

Feb 19 at 2:55 PM
@mertner Not exactly. If you have many distinct mixins, each doing a completely different thing, by SRP, they should not be re-written as members of the same class. However, they may actually be components useful to one specific class. Mixins allows you to use a form of multiple inheritance to grab many little pieces of functionality, without having to declare them all in one big class that wouldn't make sense. They're very useful for repeated concerns that don't really belong on any one class; things like Authentication, timing, logging, caching, etc...

@ickman This appears to only work when the class including the mixins has a no-parameter constructor - otherwise the compiler blows up. Is this by design? Perhaps I should raise a bug about it?