简体   繁体   中英

Mixins in TypeScript

I'm playing around with TypeScript, and I've got a couple functional mixins , Eventable and Settable , that I'd like to mixin to a Model class (pretend it's something like a Backbone.js model):

function asSettable() {
  this.get = function(key: string) {
    return this[key];
  };
  this.set = function(key: string, value) {
    this[key] = value;
    return this;
  };
}

function asEventable() {
  this.on = function(name: string, callback) {
    this._events = this._events || {};
    this._events[name] = callback;
  };
  this.trigger = function(name: string) {
    this._events[name].call(this);
  }
}

class Model {
  constructor (properties = {}) {
  };
}

asSettable.call(Model.prototype);
asEventable.call(Model.prototype);

The code above works fine, but would not compile if I tried to use one of the mixed-in methods like (new Model()).set('foo', 'bar') .

I can work around this by

  1. adding interface declarations for the mixins
  2. declaring dummy get / set / on / trigger methods in the Model declaration

Is there a clean way around the dummy declarations?

Here's one way to approach mixins using interfaces and a static create() method. Interfaces support multiple inheritance so that prevents you from having to redefine the interfaces for your mixins and the static create() method takes care of giving you back an instance of Model() as an IModel (the <any> cast is needed to supress a compiler warning.) You'll need to duplicate all of your member definitions for Model on IModel which sucks but it seems like the cleanest way to achieve what you want in the current version of TypeScript.

edit: I've identified a slightly simpler approach to supporting mixins and have even created a helper class for defining them. Details can be found over here .

function asSettable() {
  this.get = function(key: string) {
    return this[key];
  };
  this.set = function(key: string, value) {
    this[key] = value;
    return this;
  };
}

function asEventable() {
  this.on = function(name: string, callback) {
    this._events = this._events || {};
    this._events[name] = callback;
  };
  this.trigger = function(name: string) {
    this._events[name].call(this);
  }
}

class Model {
  constructor (properties = {}) {
  };

  static create(): IModel {
      return <any>new Model();
  }
}

asSettable.call(Model.prototype);
asEventable.call(Model.prototype);

interface ISettable {
    get(key: string);
    set(key: string, value);
}

interface IEvents {
    on(name: string, callback);
    trigger(name: string);
}

interface IModel extends ISettable, IEvents {
}


var x = Model.create();
x.set('foo', 'bar');

The cleanest way to do it, althought it still requires double type declarations, is to define the mixin as a module:

module Mixin {
    export function on(test) {
        alert(test);
    }
};

class TestMixin implements Mixin {
    on: (test) => void;
};


var mixed = _.extend(new TestMixin(), Mixin); // Or manually copy properties
mixed.on("hi");

An alternative to using interfaces is to hack it with classes (Although because of multiple-inheritance, you'll need to create a common-interface for the mixins):

var _:any;
var __mixes_in = _.extend; // Lookup underscore.js' extend-metod. Simply copies properties from a to b

class asSettable {
    getx(key:string) { // renamed because of token-clash in asEventAndSettable
        return this[key];
    }
    setx(key:string, value) {
        this[key] = value;
        return this;
    }
}

class asEventable {
    _events: any;
    on(name:string, callback) {
        this._events = this._events || {};
        this._events[name] = callback;
    }
    trigger(name:string) {
        this._events[name].call(this);
  }
}

class asEventAndSettable {
   // Substitute these for real type definitions
   on:any;
   trigger:any;
   getx: any;
   setx: any;
}

class Model extends asEventAndSettable {
    /// ...
}

var m = __mixes_in(new Model(), asEventable, asSettable);

// m now has all methods mixed in.

As I commented on Steven's answer, mixins really should be a TypeScript feature.

One solution is to not use the typescript class system , but just the systeme of types and interfaces, in addition to the keyword 'new'.

    //the function that create class
function Class(construct : Function, proto : Object, ...mixins : Function[]) : Function {
        //...
        return function(){};
}

module Test { 

     //the type of A
    export interface IA {
        a(str1 : string) : void;
    }

    //the class A 
    //<new () => IA>  === cast to an anonyme function constructor that create an object of type IA, 
    // the signature of the constructor is placed here, but refactoring should not work
    //Class(<IA> { === cast an anonyme object with the signature of IA (for refactoring, but the rename IDE method not work )
    export var A = <new () => IA> Class(

        //the constructor with the same signature that the cast just above
        function() { } ,

        <IA> {
            //!! the IDE does not check that the object implement all members of the interface, but create an error if an membre is not in the interface
            a : function(str : string){}
        }
    );


    //the type of B
    export interface IB {
        b() : void;
    }
    //the implementation of IB
    export class B implements IB { 
        b() { }
    }

    //the type of C
    export interface IC extends IA, IB{
        c() : void;
        mystring: string;
    }

     //the implementation of IC
    export var C = <new (mystring : string) => IC> Class(

        //public key word not work
        function(mystring : string) { 

            //problem with 'this', doesn't reference an object of type IC, why??
            //but google compiler replace self by this !!
            var self = (<IC> this);
            self.mystring = mystring;
        } ,

        <IC> {

            c : function (){},

            //override a , and call the inherited method
            a: function (str: string) {

                (<IA> A.prototype).a.call(null, 5);//problem with call and apply, signature of call and apply are static, but should be dynamic

                //so, the 'Class' function must create an method for that
                (<IA> this.$super(A)).a('');
            }

        },
        //mixins
        A, B
    );

}

var c = new Test.C('');
c.a('');
c.b();
c.c();
c.d();//ok error !

There is a new way that was build into Typescript a few years ago, called "mixin classes". It's not well covered in the docs, but they do have a well-commented example to describe the pattern well. Applied to your situation it could look something like:

type Constructor = new (...args: any[]) => {}

function Settable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    _props: Record<string, any> = {};

    get(key: string) {
      return this._props[key];
    }

    set(key: string, value: any) {
      this._props[key] = value;
      return this;
    }
  }
}

function Eventable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    _events: Record<string, () => void> = {};

    on(name: string, callback: () => void) {
      this._events[name] = callback;
    }

    trigger(name: string) {
      this._events[name].call(this);
    }
  }
}

class Model extends Settable(Eventable(Object)) {
  constructor(properties = {}) {
    super();
  }
}

This gets you the typing you wanted, so that eg you are able to call (new Model()).set('boo', 'bar') with full typing support. No dummy declarations.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM