TypeScript-Handbook icon indicating copy to clipboard operation
TypeScript-Handbook copied to clipboard

Review Mixins for language changes

Open RyanCavanaugh opened this issue 10 years ago • 3 comments

Update this file so that it is up-to-date with regard to new language features, especially around union types and ES6 features

RyanCavanaugh avatar Apr 10 '15 18:04 RyanCavanaugh

This could probably use union types, and potentially local types.

DanielRosenwasser avatar Aug 13 '15 08:08 DanielRosenwasser

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.

  1. 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 === SmartObject instead of smartObject.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.
  2. 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 Activatable compiles 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;
  }
}

DustinWehr avatar Sep 29 '15 18:09 DustinWehr

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.

Andy-MS avatar Feb 16 '18 15:02 Andy-MS