Mixin classes #13743

Merged
merged 4 commits into from Jan 30, 2017

Conversation

Projects
None yet
@ahejlsberg
Member

ahejlsberg commented Jan 30, 2017

This PR expands upon #13604 to add support for mixin classes and constructors. The PR includes type system support for the ECMAScript 2015 mixin class pattern described here and here as well as rules for combining mixin construct signatures with regular construct signatures in intersection types.

In the following, the term mixin constructor type refers to a type that has a single construct signature with a single rest argument of type any[] and an object-like return type. For example, given an object-like type X, new (...args: any[]) => X is a mixin constructor type with an instance type X.

A mixin class is a class declaration or expression that extends an expression of a type parameter type. The following rules apply to mixin class declarations:

  • The type parameter type of the extends expression must be constrained to a mixin constructor type.
  • The constructor of a mixin class (if any) must have a single rest parameter of type any[] and must use the spread operator to pass those parameters as arguments in a super(...args) call.

Given an expression Base of a parametric type T with a constraint X, a mixin class class C extends Base {...} is processed as if Base had type X and the resulting type is the intersection typeof C & T. In other words, a mixin class is represented as an intersection between the mixin class constructor type and the parametric base class constructor type.

When obtaining the construct signatures of an intersection type that contains mixin constructor types, the mixin construct signatures are discarded and their instance types are mixed into the return types of the other construct signatures in the intersection type. For example, the intersection type { new(...args: any[]) => A } & { new(s: string) => B } has a single construct signature new(s: string) => A & B.

Putting all of the above rules together in an example:

class Point {
    constructor(public x: number, public y: number) {}
}

class Person {
    constructor(public name: string) {}
}

type Constructor<T> = new(...args: any[]) => T;

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

const TaggedPoint = Tagged(Point);

let point = new TaggedPoint(10, 20);
point._tag = "hello";

class Customer extends Tagged(Person) {
    accountBalance: number;
}

let customer = new Customer("Joe");
customer._tag = "test";
customer.accountBalance = 0;

Effectively, a mixin class declaration is required to pass its constructor arguments through to the abstract base class constructor, and the result is an intersection of the declared class constructor and the base class constructor. For example, adding explicit type annotations to the code above:

interface Tagged {
    _tag: string;
}

function Tagged<T extends Constructor<{}>>(Base: T): Constructor<Tagged> & T {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

const TaggedPoint: Constructor<Tagged> & typeof Point = Tagged(Point);

let point: Tagged & Point = new TaggedPoint(10, 20);
point._tag = "hello";

The type of TaggedPoint is an intersection of two constructor types, Constructor<Tagged> and typeof Point. Since Constructor<Tagged> is a mixin constructor type, its construct signature is "mixed into" the constructor for Point. Thus, TaggedPoint has a single construct signature with the same parameter list as Point but with the return type Tagged & Point.

Mixin classes can constrain the types of classes they can mix into by specifying a construct signature return type in the constraint for the type parameter. For example, the following WithLocation function implements a subclass factory that adds a getLocation method to any class that satisfies the Point interface (i.e. that has x and y properties of type number).

interface Point {
    x: number;
    y: number;
}

const WithLocation = <T extends Constructor<Point>>(Base: T) =>
    class extends Base {
        getLocation(): [number, number] {
            return [this.x, this.y];
        }
    }

Fixes #4890.
Fixes #10261.

@pleerock

This comment has been minimized.

Show comment
Hide comment
@pleerock

pleerock Jan 30, 2017

I guess we can use multiple mixins this way?:

class Customer extends Subscribable(Scorable(Tagged(Person))) { 

Everything fits good into exist language syntax, however it also would be great to have some syntax sugar mixins, maybe even separate keyword, because this does not look good between beautiful domain models and services:

type Constructor<T> = new(...args: any[]) => T;

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {

And one another question, is it possible to do:

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
       /// ...
    }
}

class Customer extends Tagged(Person) {
    accountBalance: number;
}

let customer = new Customer("Joe");

and later on to check if customer instanceOf Tagged ? It will be really-really useful to check it in runtime to perform some operations based on it (especially when we can't do same right now with interfaces).

pleerock commented Jan 30, 2017

I guess we can use multiple mixins this way?:

class Customer extends Subscribable(Scorable(Tagged(Person))) { 

Everything fits good into exist language syntax, however it also would be great to have some syntax sugar mixins, maybe even separate keyword, because this does not look good between beautiful domain models and services:

type Constructor<T> = new(...args: any[]) => T;

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {

And one another question, is it possible to do:

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
       /// ...
    }
}

class Customer extends Tagged(Person) {
    accountBalance: number;
}

let customer = new Customer("Joe");

and later on to check if customer instanceOf Tagged ? It will be really-really useful to check it in runtime to perform some operations based on it (especially when we can't do same right now with interfaces).

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Jan 30, 2017

Member

I guess we can use multiple mixins this way?

Yes.

Everything fits good into exist language syntax, however it also would be great to have some syntax sugar mixins, maybe even separate keyword, because this does not look good between beautiful domain models and services.

We may consider adding the Constructor<T> type to lib.d.ts, but beyond that I wouldn't expect syntactic sugar for the feature.

and later on to check if someCustomer instanceOf Tagged?

You can check if something is an instance of a specific invocation of Tagged like this:

const TaggedPoint = Tagged(Point);

function check(obj: object) {
    if (obj instanceof TaggedPoint) {
        // obj narrowed to mixin type
    }
}

However, you can't use instanceof to determine if an object is some Tagged type, but you could of course write a user defined type predicate to check it.

Member

ahejlsberg commented Jan 30, 2017

I guess we can use multiple mixins this way?

Yes.

Everything fits good into exist language syntax, however it also would be great to have some syntax sugar mixins, maybe even separate keyword, because this does not look good between beautiful domain models and services.

We may consider adding the Constructor<T> type to lib.d.ts, but beyond that I wouldn't expect syntactic sugar for the feature.

and later on to check if someCustomer instanceOf Tagged?

You can check if something is an instance of a specific invocation of Tagged like this:

const TaggedPoint = Tagged(Point);

function check(obj: object) {
    if (obj instanceof TaggedPoint) {
        // obj narrowed to mixin type
    }
}

However, you can't use instanceof to determine if an object is some Tagged type, but you could of course write a user defined type predicate to check it.

@ahejlsberg ahejlsberg merged commit f32f95a into master Jan 30, 2017

4 checks passed

TypeScript Test Run typescript_node.4 Build finished.
Details
TypeScript Test Run typescript_node.6 Build finished.
Details
TypeScript Test Run typescript_node.stable Build finished.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

@ahejlsberg ahejlsberg deleted the mixinClasses branch Jan 30, 2017

@ahejlsberg ahejlsberg added this to the TypeScript 2.2 milestone Jan 30, 2017

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Jan 31, 2017

@ahejlsberg this is awesome, thanks for making this all work!

Since #4890 was where we were talking about the extends type operator, which would describe the mixin application more accurately than intersection types, should we open a new issue to track that, or it that idea shelved in favor of intersections indefinitely?

@ahejlsberg this is awesome, thanks for making this all work!

Since #4890 was where we were talking about the extends type operator, which would describe the mixin application more accurately than intersection types, should we open a new issue to track that, or it that idea shelved in favor of intersections indefinitely?

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Jan 31, 2017

@pleerock it actually is possible to get customer instanceOf Tagged working by implementing the [Symbol.hasInstance] method on Tagged, and having Tagged leave some information on the class it produces.

I talk about this here: http://justinfagnani.com/2016/01/07/enhancing-mixins-with-decorator-functions/ though some members of the V8 team have warned me against using hasInstance at all, since at least at one point it cause a global slowdown of instanceOf.

@pleerock it actually is possible to get customer instanceOf Tagged working by implementing the [Symbol.hasInstance] method on Tagged, and having Tagged leave some information on the class it produces.

I talk about this here: http://justinfagnani.com/2016/01/07/enhancing-mixins-with-decorator-functions/ though some members of the V8 team have warned me against using hasInstance at all, since at least at one point it cause a global slowdown of instanceOf.

@jmendiara jmendiara referenced this pull request in therror/therror Jan 31, 2017

Closed

Use Typescript support for mixins #9

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Jan 31, 2017

Member

@justinfagnani We considered adding a B extends A type operator (or alternatively an A with B type operator), but we decided against it because we have yet to devise a consistent error reporting scheme for such an operator.

Presumably users would expect B extends A to report errors if A and B have conflicting properties (e.g. when A has a property x: number and B has a property x: string), similarly to how we report conflicts in class and interface declarations. The extends keyword strongly implies such a check. However, when A and/or B are type variables, we don't discover such errors until B extends A is instantiated with actual types. This brings about two issues. (1) Type instantiations are deferred and occur only when they are actually needed. This is pretty much a must in any compiler implementation that supports parametric types. (2) The reasons for type instantiations are not currently tracked (and, indeed, are not easy to track). Because of these issues, errors reported from type instantiations would be inconsistent and unable to exactly pinpoint their cause in the source code. For example, errors resulting from relating types that contain properties using extends types might or might not be reported depending on whether we ever need to check the particular properties (because only then would instantiation happen). So, you might see "phantom" errors that appear and disappear in inexplicable ways. That's not a good experience.

We could of course consider having an extends type operator that doesn't report errors, but that would immediately lead to inconsistencies (because a B extends A would presumably be assignable to A, but might not actually be assignable upon instantiation).

So, the upshot of all of this is that any implementation of extends we could provide would be an unacceptable compromise. For that reason we've chosen to stay with the intersection operator. And, of course, it is always good to not have multiple ways of doing almost the same thing.

Member

ahejlsberg commented Jan 31, 2017

@justinfagnani We considered adding a B extends A type operator (or alternatively an A with B type operator), but we decided against it because we have yet to devise a consistent error reporting scheme for such an operator.

Presumably users would expect B extends A to report errors if A and B have conflicting properties (e.g. when A has a property x: number and B has a property x: string), similarly to how we report conflicts in class and interface declarations. The extends keyword strongly implies such a check. However, when A and/or B are type variables, we don't discover such errors until B extends A is instantiated with actual types. This brings about two issues. (1) Type instantiations are deferred and occur only when they are actually needed. This is pretty much a must in any compiler implementation that supports parametric types. (2) The reasons for type instantiations are not currently tracked (and, indeed, are not easy to track). Because of these issues, errors reported from type instantiations would be inconsistent and unable to exactly pinpoint their cause in the source code. For example, errors resulting from relating types that contain properties using extends types might or might not be reported depending on whether we ever need to check the particular properties (because only then would instantiation happen). So, you might see "phantom" errors that appear and disappear in inexplicable ways. That's not a good experience.

We could of course consider having an extends type operator that doesn't report errors, but that would immediately lead to inconsistencies (because a B extends A would presumably be assignable to A, but might not actually be assignable upon instantiation).

So, the upshot of all of this is that any implementation of extends we could provide would be an unacceptable compromise. For that reason we've chosen to stay with the intersection operator. And, of course, it is always good to not have multiple ways of doing almost the same thing.

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Jan 31, 2017

@ahejlsberg Thanks for the detailed explanation.

There were some comments about deferring the extends check until the types are known. Is something like that possible? And excuse my ignorance, I only hang out with compiler people from my days on Dart and from sitting near some V8 folk.

class A {
  x: number;
}

// 1: is it possible to infer here that Base must be extendable by {x: string}?
// extendableBy might be a new internal type operator?
const B = <T extends Constructor<{}>>(Base: T) => class extends Base {
  x: string;
}

// 2: is possible here to check that A is extendable by {x: string}?
// This is where the error would be reported
const C = B(A);

(by the way, this is why I was asking if we should open an issue, for this discussion. I can move it there, and it could be closed so I know it's over :) )

@ahejlsberg Thanks for the detailed explanation.

There were some comments about deferring the extends check until the types are known. Is something like that possible? And excuse my ignorance, I only hang out with compiler people from my days on Dart and from sitting near some V8 folk.

class A {
  x: number;
}

// 1: is it possible to infer here that Base must be extendable by {x: string}?
// extendableBy might be a new internal type operator?
const B = <T extends Constructor<{}>>(Base: T) => class extends Base {
  x: string;
}

// 2: is possible here to check that A is extendable by {x: string}?
// This is where the error would be reported
const C = B(A);

(by the way, this is why I was asking if we should open an issue, for this discussion. I can move it there, and it could be closed so I know it's over :) )

@mjewell

This comment has been minimized.

Show comment
Hide comment
@mjewell

mjewell Feb 6, 2017

What would the types for a function which applied a mixin look like? I couldn't find a way to get this to work:

type Constructor<T> = new(...args: any[]) => T;

interface IMixin<S extends Constructor<{}>, T> {
  (superclass: S): T; // this return type should be Constructor<T> & S?
}

function applyMixin<S extends Constructor<{}>, T>(Mixin: IMixin<S, T>, SuperClass: S) {
  return Mixin(SuperClass);
}

class SuperClass {
  x() {}
}

function Mixin<T extends Constructor<{}>>(superclass: T) {
  return class extends superclass {
    y() {}
  };
}

const A = applyMixin(Mixin, SuperClass);
const a = new A();
// a.x is not known here
// a.y is known here

const B = Mixin(SuperClass);
const b = new B();
// b.x is known here
// b.y is known here

mjewell commented Feb 6, 2017

What would the types for a function which applied a mixin look like? I couldn't find a way to get this to work:

type Constructor<T> = new(...args: any[]) => T;

interface IMixin<S extends Constructor<{}>, T> {
  (superclass: S): T; // this return type should be Constructor<T> & S?
}

function applyMixin<S extends Constructor<{}>, T>(Mixin: IMixin<S, T>, SuperClass: S) {
  return Mixin(SuperClass);
}

class SuperClass {
  x() {}
}

function Mixin<T extends Constructor<{}>>(superclass: T) {
  return class extends superclass {
    y() {}
  };
}

const A = applyMixin(Mixin, SuperClass);
const a = new A();
// a.x is not known here
// a.y is known here

const B = Mixin(SuperClass);
const b = new B();
// b.x is known here
// b.y is known here
@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 6, 2017

Member

@mjewell That's an example of where we need higher-order functions (#9366), i.e. the ability to pass a generic function as an argument to another generic function without erasing the type parameters of the first function.

Member

ahejlsberg commented Feb 6, 2017

@mjewell That's an example of where we need higher-order functions (#9366), i.e. the ability to pass a generic function as an argument to another generic function without erasing the type parameters of the first function.

@mjewell

This comment has been minimized.

Show comment
Hide comment
@mjewell

mjewell Feb 6, 2017

Thanks for the quick response. Does this also mean there is no way to return a generic class from a mixin? Something like:

class SuperClass<T> {

}

function Mixin<T extends Constructor<SuperClass>>(superclass: T) {
  return class<S> extends superclass<S> {
    y() {}
  };
}

mjewell commented Feb 6, 2017

Thanks for the quick response. Does this also mean there is no way to return a generic class from a mixin? Something like:

class SuperClass<T> {

}

function Mixin<T extends Constructor<SuperClass>>(superclass: T) {
  return class<S> extends superclass<S> {
    y() {}
  };
}

@agubler agubler referenced this pull request in dojo/widget-core Feb 7, 2017

Closed

Convert from dojo/compose to TS2.2 class mixins #327

@maier49 maier49 referenced this pull request in dojo/stores Feb 8, 2017

Closed

Convert from dojo/compose to TS2.2 class mixins #108

@electricessence

This comment has been minimized.

Show comment
Hide comment
@electricessence

electricessence Feb 8, 2017

Wow. Is this really happening?

Wow. Is this really happening?

@opensrcken

This comment has been minimized.

Show comment
Hide comment
@opensrcken

opensrcken Feb 15, 2017

@electricessence, I exclaimed aloud when I saw this. The TS team delivers again.

@electricessence, I exclaimed aloud when I saw this. The TS team delivers again.

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Feb 18, 2017

@ahejlsberg is there a way to use generic classes as base for mixins? say in your example what if we had class Point<N> { constructor (public x: N, public y: N) {} } then what?

@ahejlsberg is there a way to use generic classes as base for mixins? say in your example what if we had class Point<N> { constructor (public x: N, public y: N) {} } then what?

@unional

This comment has been minimized.

Show comment
Hide comment
@unional

unional Feb 18, 2017

Contributor

Can this be proposed and standardized in ECMAScript for es2018/2019? 🌷

Contributor

unional commented Feb 18, 2017

Can this be proposed and standardized in ECMAScript for es2018/2019? 🌷

@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser Feb 18, 2017

Member

@unional All of this is still ECMAScript 2015. The work here affects the type system side, so now TypeScript can more accurately model different scenarios related to factory constructors, and give you the same support (i.e. checking & tooling) that TypeScript gave you before in other places.

Member

DanielRosenwasser commented Feb 18, 2017

@unional All of this is still ECMAScript 2015. The work here affects the type system side, so now TypeScript can more accurately model different scenarios related to factory constructors, and give you the same support (i.e. checking & tooling) that TypeScript gave you before in other places.

@unional

This comment has been minimized.

Show comment
Hide comment
@unional

unional Feb 19, 2017

Contributor

Ar, I didn't know that class Foo extends X(Y) { } and return class extends Base { ... } is supported in ES6. Thanks! 🌷

https://jsbin.com/xoxipoqeto/edit?js,console

// in JS.
class Point {
    constructor(x, y) {}
}

class Person {
    constructor(name) {}
}

function Tagged(Base) {
    return class extends Base {
        constructor(...args) {
            super(...args);
            this._tag = "defaultTag";
        }
    }
}

const TaggedPoint = Tagged(Point);

let point = new TaggedPoint(10, 20);

class Customer extends Tagged(Person) {
}

let customer = new Customer("Joe");
customer.accountBalance = 0;
console.log(point);
console.log(customer);
Contributor

unional commented Feb 19, 2017

Ar, I didn't know that class Foo extends X(Y) { } and return class extends Base { ... } is supported in ES6. Thanks! 🌷

https://jsbin.com/xoxipoqeto/edit?js,console

// in JS.
class Point {
    constructor(x, y) {}
}

class Person {
    constructor(name) {}
}

function Tagged(Base) {
    return class extends Base {
        constructor(...args) {
            super(...args);
            this._tag = "defaultTag";
        }
    }
}

const TaggedPoint = Tagged(Point);

let point = new TaggedPoint(10, 20);

class Customer extends Tagged(Person) {
}

let customer = new Customer("Joe");
customer.accountBalance = 0;
console.log(point);
console.log(customer);
@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Apr 9, 2017

@paldepind

This comment has been minimized.

Show comment
Hide comment
@paldepind

paldepind Apr 9, 2017

Thank you for the answer @aleksey-bykov.

Is there any plans to support this? Is there an issue to track it?

Thank you for the answer @aleksey-bykov.

Is there any plans to support this? Is there an issue to track it?

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Apr 9, 2017

i don't know, i am not in the design team, i am now aware of any open issues regearding this, have you tried search?

i don't know, i am not in the design team, i am now aware of any open issues regearding this, have you tried search?

@likerRr likerRr referenced this pull request in likerRr/ts-mixin Apr 13, 2017

Open

Native mixins #1

@zzmingo

This comment has been minimized.

Show comment
Hide comment
@zzmingo

zzmingo Apr 26, 2017

Looks ugly

class Customer extends Subscribable(Scorable(Tagged(Person))) { 

It's better with a more semantic syntax, maybe this:

class Customer extends Person mixins Subscribable, Scorable, Tagged

With mixins, it's easy to understand the key world super refer to the class Person, and others all is mixin.

I know the key word mixins is not standard of javascript, but, anyone think the standard will run to this ugly syntax extends Subscribable(Scorable(Tagged(Person))) in the future?

zzmingo commented Apr 26, 2017

Looks ugly

class Customer extends Subscribable(Scorable(Tagged(Person))) { 

It's better with a more semantic syntax, maybe this:

class Customer extends Person mixins Subscribable, Scorable, Tagged

With mixins, it's easy to understand the key world super refer to the class Person, and others all is mixin.

I know the key word mixins is not standard of javascript, but, anyone think the standard will run to this ugly syntax extends Subscribable(Scorable(Tagged(Person))) in the future?

@CodySchaaf

This comment has been minimized.

Show comment
Hide comment
@CodySchaaf

CodySchaaf May 5, 2017

I was trying to see if I could clean up the syntax with a MixinBuilder (similar to what is described here http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/), but I can't seem to get the type right. Anyone have a suggestion?

type Constructor<T> = new(...args: any[]) => T;
type ConstructorG = Constructor<{}>;

export const Mix = <T  extends ConstructorG = ConstructorG>(superclass: T = class {} as T) => new MixinBuilder<T>(superclass);

class MixinBuilder<T extends ConstructorG> {
	private superclass: T;
	constructor(superclass: T) {
		this.superclass = superclass;
	}

	with<M1>(m1: M1): M1 & T;
	with<M1, M2>(m1: M1, m2: M2): M1 & M2 & T;
	with<MAll>(...mixins: Function[]): T {
		return mixins.reduce((c, mixin) => mixin(c), this.superclass) as T;
	}
}

class Person {}
class Named {}
class Programmer {}
class Cody extends Mix(Person).with(Named, Programmer) {}

error TS2510: Base constructors must all have the same return type.

CodySchaaf commented May 5, 2017

I was trying to see if I could clean up the syntax with a MixinBuilder (similar to what is described here http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/), but I can't seem to get the type right. Anyone have a suggestion?

type Constructor<T> = new(...args: any[]) => T;
type ConstructorG = Constructor<{}>;

export const Mix = <T  extends ConstructorG = ConstructorG>(superclass: T = class {} as T) => new MixinBuilder<T>(superclass);

class MixinBuilder<T extends ConstructorG> {
	private superclass: T;
	constructor(superclass: T) {
		this.superclass = superclass;
	}

	with<M1>(m1: M1): M1 & T;
	with<M1, M2>(m1: M1, m2: M2): M1 & M2 & T;
	with<MAll>(...mixins: Function[]): T {
		return mixins.reduce((c, mixin) => mixin(c), this.superclass) as T;
	}
}

class Person {}
class Named {}
class Programmer {}
class Cody extends Mix(Person).with(Named, Programmer) {}

error TS2510: Base constructors must all have the same return type.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 5, 2017

Contributor

The compiler classifies a constructor as a mix-in constructor iff it has a constructor with new (...args: any[]). The assumption here is that the mixin will forward all parameters to the other base class constructors. (more here)the classes Person, Named and Programmer do not have one of these, but rather a default constructor new ().

We should however reconsider supporting such patterns. Please file a new issue to track that.

As I noted earlier, the issue is that there are too many constructors that the compiler can not safely reduce, since it can not assume that one of them will pass the args to the others. You can work around this however using the deconstructed constructor pattern. in this you take a constructor function, extract it into two type variables using inference, one represents the instance side (call it I), and one represents the static side (call it S), then you put them back togather with a new constructor signature with (...args:any[]): I & S. here is that applied to your sample:

type Constructor<T> = new(...args: any[]) => T;

function Mix<T extends object, U extends object>(superclass: U & Constructor<T> = class { } as U & Constructor<T>) {
    return new MixinBuilder<T, U>(superclass);
}

type Properties<T> = {[K in keyof T]: T[K]};


class MixinBuilder<T extends object, U extends object> {
    private superclass: Constructor<T> & U;
    constructor(superclass: Constructor<T> & U) {
        this.superclass = superclass;
    }

    with<M1 extends object, I1 extends object>(m1: Properties<M1> & Constructor<I1>): M1 & U & Constructor<T & I1>;
    with<M1 extends object, I1 extends object, M2 extends object, I2 extends object>(m1: Properties<M1> & Constructor<I1>, m2: Properties<M2> & Constructor<I2>): M1 & M2 & U & Constructor<T & I1 & I2>;
    with<MAll>(...mixins: Function[]): Constructor<T> {
        return mixins.reduce((c, mixin) => mixin(c), this.superclass) as Constructor<T>;
    }
}
Contributor

mhegazy commented May 5, 2017

The compiler classifies a constructor as a mix-in constructor iff it has a constructor with new (...args: any[]). The assumption here is that the mixin will forward all parameters to the other base class constructors. (more here)the classes Person, Named and Programmer do not have one of these, but rather a default constructor new ().

We should however reconsider supporting such patterns. Please file a new issue to track that.

As I noted earlier, the issue is that there are too many constructors that the compiler can not safely reduce, since it can not assume that one of them will pass the args to the others. You can work around this however using the deconstructed constructor pattern. in this you take a constructor function, extract it into two type variables using inference, one represents the instance side (call it I), and one represents the static side (call it S), then you put them back togather with a new constructor signature with (...args:any[]): I & S. here is that applied to your sample:

type Constructor<T> = new(...args: any[]) => T;

function Mix<T extends object, U extends object>(superclass: U & Constructor<T> = class { } as U & Constructor<T>) {
    return new MixinBuilder<T, U>(superclass);
}

type Properties<T> = {[K in keyof T]: T[K]};


class MixinBuilder<T extends object, U extends object> {
    private superclass: Constructor<T> & U;
    constructor(superclass: Constructor<T> & U) {
        this.superclass = superclass;
    }

    with<M1 extends object, I1 extends object>(m1: Properties<M1> & Constructor<I1>): M1 & U & Constructor<T & I1>;
    with<M1 extends object, I1 extends object, M2 extends object, I2 extends object>(m1: Properties<M1> & Constructor<I1>, m2: Properties<M2> & Constructor<I2>): M1 & M2 & U & Constructor<T & I1 & I2>;
    with<MAll>(...mixins: Function[]): Constructor<T> {
        return mixins.reduce((c, mixin) => mixin(c), this.superclass) as Constructor<T>;
    }
}
@oozaa

This comment has been minimized.

Show comment
Hide comment
@oozaa

oozaa May 7, 2017

@mhegazy
Would be great if you could write an example how to use Mix<>(....).with(...) ? I made quite a few attempts but was keep getting this error.
constructor(superclass: Constructor<T> & U) { ^ TypeError: Object prototype may only be an Object or null: undefined

Thanks!

oozaa commented May 7, 2017

@mhegazy
Would be great if you could write an example how to use Mix<>(....).with(...) ? I made quite a few attempts but was keep getting this error.
constructor(superclass: Constructor<T> & U) { ^ TypeError: Object prototype may only be an Object or null: undefined

Thanks!

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 8, 2017

Contributor

@oozaa, this is an adaptation of @CodySchaaf solution, may be @CodySchaaf can share his full implementation.

Contributor

mhegazy commented May 8, 2017

@oozaa, this is an adaptation of @CodySchaaf solution, may be @CodySchaaf can share his full implementation.

@khusamov

This comment has been minimized.

Show comment
Hide comment
@khusamov

khusamov May 11, 2017

@mhegazy How to fix this error?

/**/index.js:10
        return mixins.reduce((c, mixin) => mixin(c), this.superclass);
                                           ^

TypeError: Class constructor Named cannot be invoked without 'new'
    at mixins.reduce (/**/index.js:10:44)

@mhegazy How to fix this error?

/**/index.js:10
        return mixins.reduce((c, mixin) => mixin(c), this.superclass);
                                           ^

TypeError: Class constructor Named cannot be invoked without 'new'
    at mixins.reduce (/**/index.js:10:44)
@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 11, 2017

Contributor

@mhegazy How to fix this error?

Please file an issue, provide a self-contained sample for us to be able to help.

Contributor

mhegazy commented May 11, 2017

@mhegazy How to fix this error?

Please file an issue, provide a self-contained sample for us to be able to help.

@khusamov

This comment has been minimized.

Show comment
Hide comment
@khusamov

khusamov May 11, 2017

@mhegazy I'm trying to figure out your code. But he, unfortunately, gives an error.

type Constructor<T> = new(...args: any[]) => T;

function Mix<T extends object, U extends object>(superclass: U & Constructor<T> = class { } as U & Constructor<T>) {
    return new MixinBuilder<T, U>(superclass);
}

type Properties<T> = {[K in keyof T]: T[K]};


class MixinBuilder<T extends object, U extends object> {
    private superclass: Constructor<T> & U;
    constructor(superclass: Constructor<T> & U) {
        this.superclass = superclass;
    }

    with<M1 extends object, I1 extends object>(m1: Properties<M1> & Constructor<I1>): M1 & U & Constructor<T & I1>;
    with<M1 extends object, I1 extends object, M2 extends object, I2 extends object>(m1: Properties<M1> & Constructor<I1>, m2: Properties<M2> & Constructor<I2>): M1 & M2 & U & Constructor<T & I1 & I2>;
    with<MAll>(...mixins: Function[]): Constructor<T> {
        return mixins.reduce((c, mixin) => mixin(c), this.superclass) as Constructor<T>;
    }
}

class Person {
    methodA() { console.log("methodA"); return this}
    protected methodAP() { console.log("methodAP"); return this}
    propA: number;
    static staticMethodA() { console.log("staticMethodA"); };
}
class Named {}
class Programmer {}
class Cody extends Mix(Person).with(Named, Programmer) {
    methodB() { console.log("methodB"); }
    methodBAP() { this.methodAP(); console.log("methodB"); }
    propB: number;
    static staticMethodB() { console.log("staticMethodB"); };
}


Cody.staticMethodA();
Cody.staticMethodB();
Cody.prototype
new Cody().methodA();
new Cody().methodB();

khusamov commented May 11, 2017

@mhegazy I'm trying to figure out your code. But he, unfortunately, gives an error.

type Constructor<T> = new(...args: any[]) => T;

function Mix<T extends object, U extends object>(superclass: U & Constructor<T> = class { } as U & Constructor<T>) {
    return new MixinBuilder<T, U>(superclass);
}

type Properties<T> = {[K in keyof T]: T[K]};


class MixinBuilder<T extends object, U extends object> {
    private superclass: Constructor<T> & U;
    constructor(superclass: Constructor<T> & U) {
        this.superclass = superclass;
    }

    with<M1 extends object, I1 extends object>(m1: Properties<M1> & Constructor<I1>): M1 & U & Constructor<T & I1>;
    with<M1 extends object, I1 extends object, M2 extends object, I2 extends object>(m1: Properties<M1> & Constructor<I1>, m2: Properties<M2> & Constructor<I2>): M1 & M2 & U & Constructor<T & I1 & I2>;
    with<MAll>(...mixins: Function[]): Constructor<T> {
        return mixins.reduce((c, mixin) => mixin(c), this.superclass) as Constructor<T>;
    }
}

class Person {
    methodA() { console.log("methodA"); return this}
    protected methodAP() { console.log("methodAP"); return this}
    propA: number;
    static staticMethodA() { console.log("staticMethodA"); };
}
class Named {}
class Programmer {}
class Cody extends Mix(Person).with(Named, Programmer) {
    methodB() { console.log("methodB"); }
    methodBAP() { this.methodAP(); console.log("methodB"); }
    propB: number;
    static staticMethodB() { console.log("staticMethodB"); };
}


Cody.staticMethodA();
Cody.staticMethodB();
Cody.prototype
new Cody().methodA();
new Cody().methodB();
@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 11, 2017

Contributor

not sure what error you are seeing.. here is the code above pasted in the playground

Contributor

mhegazy commented May 11, 2017

not sure what error you are seeing.. here is the code above pasted in the playground

@khusamov

This comment has been minimized.

Show comment
Hide comment
@khusamov

khusamov May 11, 2017

@mhegazy I compile in ES6, then run in NodeJS. At the compilation stage, there are no errors. But at runtime there is an error:

/**/index.js:10
        return mixins.reduce((c, mixin) => mixin(c), this.superclass);
                                           ^

TypeError: Class constructor Named cannot be invoked without 'new'
    at mixins.reduce (/**/index.js:10:44)

@mhegazy I compile in ES6, then run in NodeJS. At the compilation stage, there are no errors. But at runtime there is an error:

/**/index.js:10
        return mixins.reduce((c, mixin) => mixin(c), this.superclass);
                                           ^

TypeError: Class constructor Named cannot be invoked without 'new'
    at mixins.reduce (/**/index.js:10:44)
@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 11, 2017

Contributor

ah.. as i said erlier, this is an adaptation to @CodySchaaf's implementation to address a specific issue with getting it to type check correctly. i can not claim that i know how it behaves or should behave at runtime.

Contributor

mhegazy commented May 11, 2017

ah.. as i said erlier, this is an adaptation to @CodySchaaf's implementation to address a specific issue with getting it to type check correctly. i can not claim that i know how it behaves or should behave at runtime.

@CodySchaaf

This comment has been minimized.

Show comment
Hide comment
@CodySchaaf

CodySchaaf May 11, 2017

My example came from http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/#improvingthesyntax

I used a bad choice for the mixins, they still need to be in the function syntax:

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

I tried playing with it briefly, and could not get the typing to check out as well.

My example came from http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/#improvingthesyntax

I used a bad choice for the mixins, they still need to be in the function syntax:

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

I tried playing with it briefly, and could not get the typing to check out as well.

@khusamov

This comment has been minimized.

Show comment
Hide comment
@khusamov

khusamov May 13, 2017

@mhegazy Can you help me?

How to export mixin class? The following code

export type Constructor<T = object> = new(...args: any[]) => T;
export default <S extends Constructor>(Superclass: S) => class Pluggable extends Superclass {...}

generates this error

error TS4082: Default export of the module has or is using private name 'Pluggable'.

@mhegazy Can you help me?

How to export mixin class? The following code

export type Constructor<T = object> = new(...args: any[]) => T;
export default <S extends Constructor>(Superclass: S) => class Pluggable extends Superclass {...}

generates this error

error TS4082: Default export of the module has or is using private name 'Pluggable'.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 13, 2017

Contributor

Looks like you are running into #15066.

The workaround is to give your function an explicit type annoation using intersection types, i.e.:

export default <S extends Constructor>(Superclass: S): S & Constructor<{ <props Pluggable adds>}> => class Pluggable extends Superclass { }
Contributor

mhegazy commented May 13, 2017

Looks like you are running into #15066.

The workaround is to give your function an explicit type annoation using intersection types, i.e.:

export default <S extends Constructor>(Superclass: S): S & Constructor<{ <props Pluggable adds>}> => class Pluggable extends Superclass { }
@khusamov

This comment has been minimized.

Show comment
Hide comment
@khusamov

khusamov May 15, 2017

@mhegazy Thank you! It helped, but there was the following strange problem.

File Pluggable.ts with mixin:

import AbstractPluginManager from './AbstractPluginManager';
export type Constructor<T = object> = new(...args: any[]) => T;
export interface Pluggable {
    pluginManager: AbstractPluginManager;
}
export default <S extends Constructor>(Superclass: S): Constructor<Pluggable> & S => class extends Superclass {
    private _pluginManager: AbstractPluginManager;
    get pluginManager(): AbstractPluginManager {
		return this._pluginManager ? this._pluginManager : this._pluginManager = new AbstractPluginManager(this);
	}
}

The file where this mixin is used:

import Pluggable from '../plugin/Pluggable';
export default class AbstractRequestHandler extends Pluggable(EventEmitter) { ...

The last line caused two errors:

error TS4093: 'extends' clause of exported class 'AbstractRequestHandler' refers to a type whose name cannot be referenced.
error TS4020: 'extends' clause of exported class 'AbstractRequestHandler' has or is using private name 'Pluggable'.

@mhegazy Thank you! It helped, but there was the following strange problem.

File Pluggable.ts with mixin:

import AbstractPluginManager from './AbstractPluginManager';
export type Constructor<T = object> = new(...args: any[]) => T;
export interface Pluggable {
    pluginManager: AbstractPluginManager;
}
export default <S extends Constructor>(Superclass: S): Constructor<Pluggable> & S => class extends Superclass {
    private _pluginManager: AbstractPluginManager;
    get pluginManager(): AbstractPluginManager {
		return this._pluginManager ? this._pluginManager : this._pluginManager = new AbstractPluginManager(this);
	}
}

The file where this mixin is used:

import Pluggable from '../plugin/Pluggable';
export default class AbstractRequestHandler extends Pluggable(EventEmitter) { ...

The last line caused two errors:

error TS4093: 'extends' clause of exported class 'AbstractRequestHandler' refers to a type whose name cannot be referenced.
error TS4020: 'extends' clause of exported class 'AbstractRequestHandler' has or is using private name 'Pluggable'.
@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser May 15, 2017

Member

@khusamov looks like a bug, can you open up a separate issue?

Member

DanielRosenwasser commented May 15, 2017

@khusamov looks like a bug, can you open up a separate issue?

@khusamov

This comment has been minimized.

Show comment
Hide comment
@dmitryxcom

This comment has been minimized.

Show comment
Hide comment
@dmitryxcom

dmitryxcom May 25, 2017

I'm trying to understand why inference works better than explicit typing here.
I'm new to TS so maybe I'm missing some core understanding of what TS does with
const a: Type = ...;
but it feels like either it should work with explicit typing or there's something wrong with Constructor & T; return type (and it doesn't quite capture what's going on).

class Person {
    constructor(public name: string) {}
}

type Constructor<T> = new(...args: any[]) => T;

type MixinFn<MixinInterface> = <T extends Constructor<{}>>(superclass: T) => Constructor<MixinInterface> & T;

interface Tagable {
    tag: (tag: string) => void;
}

// Why doesn't this work?
type TagableMxinFn = MixinFn<Tagable>;
const Tagable: TagableMxinFn = <T extends Constructor<{}>>(superclass: T) => { // T is not <Constructor<{}>>
    return class extends superclass {
        // no typechecking of rerturn type; must use implements
    }
};

const TagablePerson = Tagable(Person);
let foo  = new TagablePerson('a');
// foo.tag(); error

// While this does (and you can actually drop the return type.
function Tagable2<T extends Constructor<{}>>(superclass: T) {
  return class extends superclass {
      // implements Tagable is enforced here
      tag(tag: string) {
          console.log(`tagged with ${tag}`);
      }
  }
}

const Tagable2Person = Tagable2(Person);
let bar = new Tagable2Person('s');
bar.name;
bar.tag('s'); // all good, autocompleted and checked

export {};

I'm trying to understand why inference works better than explicit typing here.
I'm new to TS so maybe I'm missing some core understanding of what TS does with
const a: Type = ...;
but it feels like either it should work with explicit typing or there's something wrong with Constructor & T; return type (and it doesn't quite capture what's going on).

class Person {
    constructor(public name: string) {}
}

type Constructor<T> = new(...args: any[]) => T;

type MixinFn<MixinInterface> = <T extends Constructor<{}>>(superclass: T) => Constructor<MixinInterface> & T;

interface Tagable {
    tag: (tag: string) => void;
}

// Why doesn't this work?
type TagableMxinFn = MixinFn<Tagable>;
const Tagable: TagableMxinFn = <T extends Constructor<{}>>(superclass: T) => { // T is not <Constructor<{}>>
    return class extends superclass {
        // no typechecking of rerturn type; must use implements
    }
};

const TagablePerson = Tagable(Person);
let foo  = new TagablePerson('a');
// foo.tag(); error

// While this does (and you can actually drop the return type.
function Tagable2<T extends Constructor<{}>>(superclass: T) {
  return class extends superclass {
      // implements Tagable is enforced here
      tag(tag: string) {
          console.log(`tagged with ${tag}`);
      }
  }
}

const Tagable2Person = Tagable2(Person);
let bar = new Tagable2Person('s');
bar.name;
bar.tag('s'); // all good, autocompleted and checked

export {};
@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser May 25, 2017

Member

@dmitryxcom please open up a separate issue if you think you've found a bug. Also, that works for me using the nightlies.

Member

DanielRosenwasser commented May 25, 2017

@dmitryxcom please open up a separate issue if you think you've found a bug. Also, that works for me using the nightlies.

@JustASquid

This comment has been minimized.

Show comment
Hide comment
@JustASquid

JustASquid Aug 9, 2017

Just to be clear - it's flat out impossible right now to have a generic mixin, or to mix into a generic type? This is really limiting the usefulness of mixins for me, are there any workarounds?

Just to be clear - it's flat out impossible right now to have a generic mixin, or to mix into a generic type? This is really limiting the usefulness of mixins for me, are there any workarounds?

@mattdsteele mattdsteele referenced this pull request in ionic-team/stencil Jan 30, 2018

Closed

Decorating an extended element breaks in polyfilled browsers #459

1 of 3 tasks complete

@zixia zixia referenced this pull request in Chatie/wechaty Apr 22, 2018

Closed

[todo] allow Wechaty to be multi-instance #518

5 of 5 tasks complete
@granteagon

This comment has been minimized.

Show comment
Hide comment
@granteagon

granteagon Apr 25, 2018

@zzmingo I like it but I think mixin would be better than mixins.

class SuperHero mixin Person, CanFly, SuperStrength {}

granteagon commented Apr 25, 2018

@zzmingo I like it but I think mixin would be better than mixins.

class SuperHero mixin Person, CanFly, SuperStrength {}
@granteagon

This comment has been minimized.

Show comment
Hide comment
@granteagon

granteagon Apr 25, 2018

We could also just do something like C#:

class SuperHero extends Person, CanFly, SuperStrength {}

Would work just about the same.

We could also just do something like C#:

class SuperHero extends Person, CanFly, SuperStrength {}

Would work just about the same.

@justinfagnani

This comment has been minimized.

Show comment
Hide comment
@justinfagnani

justinfagnani Apr 25, 2018

@granteagon I don't think the TypeScript team wants to add new additional features outside of he type system. There's too much danger of incompatibilities with future JavaScript evolution.

FWIW, I have a proposal to TC39 to add mixins to JavaScript: https://github.com/justinfagnani/proposal-mixins

As that progresses, the TypeScript team could consider adding support. I'm not sure when they start adding proposed features, but probably not earlier than stage 3.

@granteagon I don't think the TypeScript team wants to add new additional features outside of he type system. There's too much danger of incompatibilities with future JavaScript evolution.

FWIW, I have a proposal to TC39 to add mixins to JavaScript: https://github.com/justinfagnani/proposal-mixins

As that progresses, the TypeScript team could consider adding support. I'm not sure when they start adding proposed features, but probably not earlier than stage 3.

@granteagon

This comment has been minimized.

Show comment
Hide comment
@granteagon

granteagon Apr 25, 2018

@justinfagnani Mixins seem to solve a lot of inflexibility with OOP. They also somewhat bring the functional programming world and OOP world closer together. If there's a way I can show support for you proposal, let me know.

@justinfagnani Mixins seem to solve a lot of inflexibility with OOP. They also somewhat bring the functional programming world and OOP world closer together. If there's a way I can show support for you proposal, let me know.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Jun 17, 2018

Hello everyone, I'm trying to figure out how to type a class-factory. I've an API that takes an object literal, and effectively returns the equivalent of a class.

const Foo = Class(({Private}) => ({
  someMethod() {
    Private(this).somePrivateMethod()
  },
  private: {
    somePrivateMethod() { ... },
  }
}))

const foo = new Foo
foo.someMethod()

where Class is a function that accepts an object literal (or a function that returns an object literal) and returns what is effectively equivalent to a class {}. I am wondering how to begin typing this (if possible).

I made a StackOverflow question about it, and I figured to share it here because the people that know most about class factories in TypeScript are right here. :)

trusktr commented Jun 17, 2018

Hello everyone, I'm trying to figure out how to type a class-factory. I've an API that takes an object literal, and effectively returns the equivalent of a class.

const Foo = Class(({Private}) => ({
  someMethod() {
    Private(this).somePrivateMethod()
  },
  private: {
    somePrivateMethod() { ... },
  }
}))

const foo = new Foo
foo.someMethod()

where Class is a function that accepts an object literal (or a function that returns an object literal) and returns what is effectively equivalent to a class {}. I am wondering how to begin typing this (if possible).

I made a StackOverflow question about it, and I figured to share it here because the people that know most about class factories in TypeScript are right here. :)

@unional

This comment has been minimized.

Show comment
Hide comment
@unional

unional Jun 17, 2018

Contributor

@trusktr Have not yet think deeply in how to solve your problem, but you might encounter this: #17388 when you create your solution. 🌷

Contributor

unional commented Jun 17, 2018

@trusktr Have not yet think deeply in how to solve your problem, but you might encounter this: #17388 when you create your solution. 🌷

@pleerock

This comment has been minimized.

Show comment
Hide comment
@pleerock

pleerock Jun 19, 2018

In my opinion one of the issues of classes in javascript is absence of mixins. People can do object merging and destructing with plain javascript objects and lot of people simply use factory functions which produce pojos and have all javascript flexibility.

Although it is possible to do mixins right now (this PR) but usage is ugly and probably is used by people on some edge case scenarios.

In my opinion one of the issues of classes in javascript is absence of mixins. People can do object merging and destructing with plain javascript objects and lot of people simply use factory functions which produce pojos and have all javascript flexibility.

Although it is possible to do mixins right now (this PR) but usage is ugly and probably is used by people on some edge case scenarios.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Jun 29, 2018

I agree, using TypeScript adds a lot of complexity when trying to do stuff like this that is otherwise simple in plain JavaScript.

trusktr commented Jun 29, 2018

I agree, using TypeScript adds a lot of complexity when trying to do stuff like this that is otherwise simple in plain JavaScript.

@granteagon

This comment has been minimized.

Show comment
Hide comment
@granteagon

granteagon Jun 29, 2018

Seems like we are getting off topic.

Seems like we are getting off topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment