Review Mixins for language changes
Update this file so that it is up-to-date with regard to new language features, especially around union types and ES6 features
This could probably use union types, and potentially local types.
I took some time (too much) to refine the Mixin pattern I've been using, with the intention of offering to write a new version of http://www.typescriptlang.org/Handbook#mixins if you guys like it. I think it's a good use of user-defined type guard functions. I'll give a short summary, then some items of differences between the new pattern and the Handbook's, and then the example from the Handbook rewritten in terms of the new pattern. I have no opinion about naming conventions, so please do suggest alternatives.
For the summary I'll use the example as a reference. The new pattern produces a namespace ActivatableMixin with three visible members:
-An interface, ActivatableInterface
-A function usedBy, which tests if an object's class uses the mixin. This is the main improvement over the Handbook pattern.
-A function mixInto, such that ActivatableMixin.mixInto(C) does everything that the Handbook's applyMixins(C, [Activatable]) does, and additionally stores the class C, so we can later use (x instanceof C) in ActivatableMixin.usedBy.
In addition to the usedBy function, I'm suggesting two small alterations to prevent some surprising behavior of the current Handbook pattern that bit me.
- Added an option to exclude "constructor" from the mixed-in property names. In the code below (see makeMixinApplicator) it defaults to true. When it's excluded, you have
smartObject.constructor === SmartObjectinstead ofsmartObject.constructor === <constructor of last-supplied mixin>, which I think is less surprising, at least for someone like me who didn't know the JS prototype system well when first learning TS. - The implementation of the mixin is hidden. This is so that a new TS user doesn't mistakenly think they can use instanceof to check if an object implements a mixin. It's an easy mistake to make since
smartObject instanceof Activatablecompiles fine.
namespace DisposableMixin {
var classesUsingThis : Function[] = [];
export interface DisposableInterface {
isDisposed: boolean;
dispose() : void;
}
class DisposableImplementation implements DisposableInterface {
isDisposed:boolean;
dispose():void {
this.isDisposed = true;
}
}
export var mixInto = makeMixinApplicator(DisposableImplementation, classesUsingThis);
export var usedBy = makeMixinInstanceChecker<DisposableInterface>(classesUsingThis);
}
namespace ActivatableMixin {
var classesUsingThis : Function[] = [];
export interface ActivatableInterface {
isActive: boolean
activate() : void
deactivate() : void
}
class ActivatableImplementation implements ActivatableInterface {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
export var mixInto = makeMixinApplicator(ActivatableImplementation, classesUsingThis);
export var isActivatable = makeMixinInstanceChecker<ActivatableInterface>(classesUsingThis);
}
import DisposableInterface = DisposableMixin.DisposableInterface;
import ActivatableInterface = ActivatableMixin.ActivatableInterface;
class SmartObject implements DisposableInterface, ActivatableInterface {
constructor() {
setInterval(() => console.log("activated: " + this.isActive + " | disposed: " + this.isDisposed), 500);
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
DisposableMixin.mixInto(SmartObject);
ActivatableMixin.mixInto(SmartObject);
var smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
function makeMixinApplicator(mixinImplementation:any, classesUsingMixin:any[], exclude_constructor=true) : (c:any) => void {
return (c:any) => {
classesUsingMixin.push(c);
Object.getOwnPropertyNames(mixinImplementation.prototype).forEach(name => {
if(!exclude_constructor || name !== "constructor") {
(<any>c.prototype)[name] = (<any>mixinImplementation.prototype)[name];
}
})
};
}
function makeMixinInstanceChecker<T>(classesUsingMixin:Function[]) : ( (x:any) => x is T ) {
return (x:any) : x is T => {
for(let i=0; i<classesUsingMixin.length; i++) {
if(x instanceof classesUsingMixin[i])
return true;
}
return false;
}
}
We should include documentation like https://blog.mariusschulz.com/2017/05/26/typescript-2-2-mixin-classes -- our current documentation makes no mention of being able to do that.