-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mixin classes #13743
Mixin classes #13743
Conversation
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 |
Yes.
We may consider adding the
You can check if something is an instance of a specific invocation of const TaggedPoint = Tagged(Point);
function check(obj: object) {
if (obj instanceof TaggedPoint) {
// obj narrowed to mixin type
}
} However, you can't use |
@ahejlsberg this is awesome, thanks for making this all work! Since #4890 was where we were talking about the |
@pleerock it actually is possible to get 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 |
@justinfagnani We considered adding a Presumably users would expect We could of course consider having an So, the upshot of all of this is that any implementation of |
@ahejlsberg Thanks for the detailed explanation. There were some comments about deferring the 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 :) ) |
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 |
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() {}
};
} |
Wow. Is this really happening? |
@zzmingo I like it but I think class SuperHero mixin Person, CanFly, SuperStrength {} |
We could also just do something like C#: class SuperHero extends Person, CanFly, SuperStrength {} Would work just about the same. |
@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. |
@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. |
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 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. :) |
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. |
I agree, using TypeScript adds a lot of complexity when trying to do stuff like this that is otherwise simple in plain JavaScript. |
Seems like we are getting off topic. |
@trusktr there is a little known typescript type mostly because its undocumented that will allow you type that called ThisType And all i solved the Mixin problem today in a correctly typed implementation. The underlying problem is that the "this" of a class cannot be changed unless via a super-type or itself, this limits and solution because what you want is essentially to manipulate the type of "this" within the class. EDIT: Thought i'd intercept here but i know you can manipulate the "this" within a function but i'm talking about within classes as a whole. Understanding that i wrote this with some help.
And the implementation.....
I feel like this should put this request to rest, it doesn't need special syntax or some hieroglyphics solution that hacks together over complicated typings. |
i can't believe that this adventure is officially called "mixins":
and all this is happening in a broad daylight when much better results can be achieved in plain JS by simply calling a bunch of initializers on a bare object this is a notorious case of where "support idiomatic javascript code" goal is violated so does typescript have mixins? no it does not |
I did an adaptation of @ShanonJackson's solution and got a pseudo form of generics working. @JustASquid @Aleksey-Bykov I have the same issue as you guys and this seems to be the closest thing I got to generics. This seems to be somewhat "okay" in terms of being able to have stable classes and mixing those in - instead of defining mixin functions with anonymous classes. export type Constructor<T = {}> = new (...args: any[]) => T;
/* turns A | B | C into A & B & C */
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
/* merges constructor types - self explanatory */
type MergeConstructorTypes<T extends Array<Constructor<any>>> =
UnionToIntersection<InstanceType<T[number]>>;
export function Mixin<C1>(ctor1: Constructor<C1>): Constructor<C1>;
export function Mixin<C1, C2>(
ctor1: Constructor<C1>,
ctor2: Constructor<C2>,
): Constructor<C1 & C2>;
export function Mixin<C1, C2, C3>(
ctor1: Constructor<C1>,
ctor2: Constructor<C2>,
ctor3: Constructor<C3>,
): Constructor<C1 & C2 & C3>;
export function Mixin<C1, C2, C3>(
ctor1: Constructor<C1>,
ctor2: Constructor<C2>,
ctor3: Constructor<C3>,
): Constructor<C1 & C2 & C3>;
export function Mixin<C1, C2, C3, C4>(
ctor1: Constructor<C1>,
ctor2: Constructor<C2>,
ctor3: Constructor<C3>,
ctor4: Constructor<C4>,
): Constructor<C1 & C2 & C3 & C4>;
export function Mixin<C1, C2, C3, C4, C5>(
ctor1: Constructor<C1>,
ctor2: Constructor<C2>,
ctor3: Constructor<C3>,
ctor4: Constructor<C4>,
ctor5: Constructor<C5>,
): Constructor<C1 & C2 & C3 & C4 & C5>;
export function Mixin() {
const constructors = [].slice.call(arguments);
return constructors.reduce((cls, mixin, idx) => {
if (!idx) {
return mixin;
}
const mixedClass = class extends mixin {
constructor(...args: any[]) {
super(...args);
cls.call(this, args);
}
};
Object.assign(mixedClass.prototype, cls.prototype, mixin.prototype);
return mixedClass;
});
} Example: class A<T> {
a = 1;
b = 3;
genericFuncFromA(some: T) {
}
}
class B {
c = 2;
duck() {
}
}
class E {
d = 4;
quack() {
}
}
class C<T> extends Mixin(A, B, E) {
something(testing: T) {
}
};
// retype the generic classes you care about
type _C<T> = C<T> & A<T>;
const d: _C<string> = new C<string>(); All the instance variables/methods come through great: The generics are somewhat working...: Can't get out of the single base limitation either. Anyone might have ideas to how this could be achieved? |
its also easily possible to adapt the solution to use Objects instead of classes, or both. |
I implemented a solution to the generics problem in my ts-mixer library, but the solution can easily be adapted if you don't want to take on a (albeit, small) dependency. Essentially, the trick is to use class decorators (which can't alter types) to apply the mixins "on the JavaScript side" in conjunction with interface merging to coerce the proper class type "on the TypeScript side." Because TypeScript is blind to changes made by class decorators, the interface merging will work without conflicts: import {MixinDecorator} from 'ts-mixer';
// Some generic classes
class GenericClassA<T> {
testA(input: T) {}
}
class GenericClassB<T> {
testB(input: T) {}
}
// Class decorator/interface merging trick to create the generic mixed class
@MixinDecorator(GenericClassA, GenericClassB)
class Mixed<A, B> {
newMethod(a: A, b: B) {}
}
interface Mixed<A, B> extends GenericClassA<A>, GenericClassB<B> {}
let mm = new Mixed<string, number>();
mm.testA('test'); // ok
// mm.testA(2); // will cause error
mm.testB(2); // ok
// mm.testB('test'); // will cause error More info on this exploit is available in the ts-mixer docs. If you prefer not to use my library, a potential function MixinDecorator(...constructors) {
return function<T, U extends T>(baseClass: T): U {
// `Mixin` is assumed to come from one of the solutions above.
return class Mixed extends Mixin(baseClass, ...constructors) {} as unknown as U;
};
} I'd love feedback if anyone has any. 🙂 |
@ahejlsberg, I am the author of @northscaler/mutrait, which enables stateful traits in JavaScript. I'm trying to use what this PR enables in order to implement a similar thing in TypeScript, but I'm still not sure if the language's type system permits it. I have a github repo demonstrating the issue at https://github.com/matthewadams/typescript-trait-test. TL;DR: I'm basically trying to enable a simple way for folks to define traits & enable their classes to express them, like This leverages the subclass factory pattern you describe at the top of this issue, but attempts to do so in a way that allows a class to override functionality. Your example illustrates that you can add functionality to a class, but you can't customize it easily because the subclass factory-created class extends the class receiving the functionality, instead of the other way around. This works fine in JavaScript using If you
Thanks in advance for this. Because we leverage traits in many of our projects, this is preventing us from using TypeScript more often. |
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 typeX
,new (...args: any[]) => X
is a mixin constructor type with an instance typeX
.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:extends
expression must be constrained to a mixin constructor type.any[]
and must use the spread operator to pass those parameters as arguments in asuper(...args)
call.Given an expression
Base
of a parametric typeT
with a constraintX
, a mixin classclass C extends Base {...}
is processed as ifBase
had typeX
and the resulting type is the intersectiontypeof 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 signaturenew(s: string) => A & B
.Putting all of the above rules together in an example:
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:
The type of
TaggedPoint
is an intersection of two constructor types,Constructor<Tagged>
andtypeof Point
. SinceConstructor<Tagged>
is a mixin constructor type, its construct signature is "mixed into" the constructor forPoint
. Thus,TaggedPoint
has a single construct signature with the same parameter list asPoint
but with the return typeTagged & 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 agetLocation
method to any class that satisfies thePoint
interface (i.e. that hasx
andy
properties of typenumber
).Fixes #4890.
Fixes #10261.