-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
feature request: support for mixins composed from other mixins. #32080
Comments
@trusktr For your first example, you just need a couple of type annotations to get rid of errors: type Ctor<T> = { new(): T }
function FooMixin(Base: Ctor<{}>) {
return class Foo extends Base {
foo = 'foo'
}
}
function BarMixin(Base: Ctor<{}>) {
return class Bar extends FooMixin(Base) {
test() {
console.log(this.foo) // this.foo is obviously inherited from FooMixin!
// ^--- This shoud not be an error!
}
}
} For your second example, you can't use function FooMixin<T extends new (...args:any[]) => HTMLElement>(Base: T) {
return class Foo extends Base {
foo = 'foo'
test() {
this.setAttribute('foo', 'bar')
}
}
} I haven't read your full example or @dragomirtitian's suggested workaround on SO. Let me do that now. |
I don't quite understand. Why does your first example work, and mine (with the |
How come when I change your example from type Ctor<T> = { new(): T }
function FooMixin(Base: Ctor<{}>) {
return class Foo extends Base {
foo = 'foo'
}
} to type Ctor<T> = { new(): T }
function FooMixin<T extends Ctor<{}>>(Base: T) {
return class Foo extends Base { // ERROR, Type 'T' is not a constructor function type.
foo = 'foo'
}
} it no longer works? |
There's a hard-coded requirement the mixin's type must be an object type, which Using The only reason you'd need a type parameter is to make other parameters of FooMixin use type |
@sandersn I tried your recommendation with the type Constructor<T = any, A extends any[] = any[]> = new (...a: A) => T
function WithUpdateMixin<T extends Constructor<HTMLElement>>(Base: T = HTMLElement) { but with the
It doesn't make sense to me. How does |
Here's another example of better support needed for mixins. In my previous comment, But the So in my code (converting from JavaScript), I have the following conditional, in case the if (super.attributeChangedCallback) {
super.attributeChangedCallback(name, oldValue, newValue)
} but TypeScript gives the error:
Obviously it doesn't exist on HTMLElement. I also tried
but it is invalid syntax for TS. Same problem with if (super.connectedCallback) {
super.connectedCallback()
} etc. I also tried putting How can we make this work? |
Alright, I'm trying to work around the problem by forcing a cast, but it isn't making sense. I'm trying: type PossibleCustomElement<T = HTMLElement> = T & {
connectedCallback?(): void
disconnectedCallback?(): void
adoptedCallback?(): void
attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void
}
function WithUpdateMixin<T extends Constructor<HTMLElement>>(Base: T = HTMLElement as any) {
return class WithUpdate extends (Base as PossibleCustomElement<T>) {
// ^--- HERE and it says
Clearly
EDIT: Okay, I see type PossibleCustomElement<T extends HTMLElement> = T & {
connectedCallback?(): void
disconnectedCallback?(): void
adoptedCallback?(): void
attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void
}
type PossibleCustomElementConstructor<T extends HTMLElement> = Constructor<PossibleCustomElement<T>>
function WithUpdateMixin<T extends Constructor<HTMLElement>>(Base: T = HTMLElement as any) {
return class WithUpdate extends ((Base as unknown) as PossibleCustomElementConstructor<T>) { and now the error is:
So now my question seems to be valid: Should it be able to understand that an instance of |
Ah! I figured it out. The problem was: I was intuitively thinking that the constraint So, I figured out that if I explicitly specify the constrained type, instead of intuitively passing T along, then it works, like follows: type PossibleCustomElement<T extends HTMLElement> = T & {
connectedCallback?(): void
disconnectedCallback?(): void
adoptedCallback?(): void
attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void
}
type PossibleCustomElementConstructor<T extends HTMLElement> = Constructor<PossibleCustomElement<T>>
type HTMLElementConstructor = Constructor<HTMLElement>
function WithUpdateMixin<T extends HTMLElementConstructor>(Base: T = HTMLElement as any) {
return class WithUpdate extends ((Base as unknown) as PossibleCustomElementConstructor<HTMLElement>) {
connectedCallback() {
if (super.connectedCallback) { // ----- IT WORKS!
super.connectedCallback()
}
// ...
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (super.attributeChangedCallback) { // ----- IT WORKS!
super.attributeChangedCallback(name, oldValue, newValue)
}
// ...
}
}
} And now that makes sense: I know that the type is at least I think it would make sense for TypeScript to infer what I was trying to do by passing But I still don't like the |
Now I need to figure out if the main thing in the original post is possible: making mixins composed of mixins... The issue I forsee is that WithUpdate could be mixed with other mixins that don't necessarily extend from HTMLElement, so that later the composed mixin could finally be mixed with a subclass that does extend HTMLElement. |
Thanks for your hints about Basically, here's what a simplified version looks like: type Constructor<T = object, A extends any[] = any[]> = new (...a: A) => T
function FooMixin<T extends Constructor>(Base: T) {
class Foo extends Base {
foo = 'foo'
}
return Foo as typeof Foo & T
}
function BarMixin<T extends Constructor>(Base: T) {
class Bar extends FooMixin(Base as unknown as Constructor) {
test() {
console.log(this.foo)
}
}
return Bar as typeof Bar & T
} The |
@trusktr What further action is required here in your judgment? |
@RyanCavanaugh I think that Mixins need to be made easier. Taking the previous comment's example,
It gets more complicated when adding more mixins into the mix, when the constraint on T is more specific, when needing to include static properties in the return type, etc. For a look at how complicated and brittle the mixin boilerplates can get, see for example infamous/src/core/Node.ts (you should be able to clone, npm install, and then see all the types in VS Code if there hasn't been any breaking changes in |
I closed #32004 as a duplicate of this one. |
It'd be great if mixins in TypeScript were easier. Here's another playground example, and the code: type AnyCtor = new (...a: any[]) => any
function Foo<T extends AnyCtor>(Base: T) {
return class Foo extends Base {
foo() {}
}
}
function Bar<T extends AnyCtor>(Base: T) {
return class Bar extends Base {
bar() {}
}
}
function One<T extends AnyCtor>(Base: T) {
return class One extends Base {
one() {}
}
}
function Two<T extends AnyCtor>(Base: T) {
return class Two extends Base {
two() {}
}
}
function Three<T extends AnyCtor>(Base: T) {
return class Three extends One(Two(Base)) {
three() {}
}
}
class MyClass extends Three(Foo(Bar(Object))) {
test() {
this.foo()
this.bar()
// @ts-expect-error
this.one() // Uh oh! This is type 'any', and there is no error despite noImplicitAny being set to true!
// @ts-expect-error
this.two() // Uh oh! This is type 'any', and there is no error despite noImplicitAny being set to true!
this.three()
console.log('no errors')
}
}
const m = new MyClass()
m.test() In particular, notice that |
+1 |
OMG what a nightmare A time sink microsoft/TypeScript#32080
I've been experimenting a bit with mixins lately and believe I've run into a related case. As the example code might be a bit large due to the setup, a brief explanation: I'm looking to create mixins for a class intended for serialization and deserialization in certain cases. Serialization of mixin data isn't too hard of an ask, but the deserialization process for a mixin-enhanced version of the class - that seems to be the hard part, as I want to do it through To be explicit, I consider a requirement of preknowledge of the set of mixins involved to be reasonable. (No mixin-inference during deserialization.) I'm looking to deserialize from the final, fully mixed-in class. I believe I've actually found a way forward on this, though there's one last caveat I wasn't able to completely polish. I've kept a bit of noise in case the variations on things I attempted leading up to this could be enlightening. // Based on https://stackoverflow.com/a/43723730
interface ConfigConstructor {
restoreFromJSON: (obj: any) => any;
_loadFromJSON: (target: ConfigImpl, obj: any) => ConfigImpl;
new (a: string, b: string /*...args: any[]*/): ConfigImpl;
}
class ConfigImpl {
// Offline, I can make these private with exposing `get`-ter properties...
// ...but it seems the TS Playground isn't a fan of having them private.
public name: string;
public title: string;
constructor(name?: string, title?: string) {
// // Supports the `restoreFromJSON` method if not using optional params.
// if(arguments.length == 0) {
// this._name = '';
// this._title = '';
// return;
// }
this.name = name ?? '';
this.title = title ?? '';
}
static restoreFromJSON = function (obj: any) {
const restored = new ConfigImpl('', '');
return ConfigImpl._loadFromJSON(restored, obj);
}
static _loadFromJSON = function (target: ConfigImpl, obj: any) {
target.name = obj.name;
target.title = obj.title;
return target;
}
}
type AnyConstructor<A = object> = new (...input: any[]) => A
const Config: AnyConstructor<ConfigImpl> & ConfigConstructor = ConfigImpl;
Config.restoreFromJSON;
const dummy = new Config("dummy", "object");
dummy.name;
dummy.title;
// Defining the mixin is... not the easiest, but this is the only ugly part. I think.
// function IntermediateMixin<T extends AnyConstructor<typeof Config> & Omit<typeof Config, 'new'>>(Base: T) { // falls over. :(
function IntermediateMixin<T extends AnyConstructor<ConfigImpl> & Omit<typeof Config, 'new'>>(Base: T) { // works!
// function IntermediateMixin<T extends AnyConstructor<ConfigImpl> & Omit<ConfigConstructor, 'new'>>(Base: T) { // works!
return class MixinCore extends Base {
public counter: number = 0;
// Utilizes the fact that constructors themselves inherit from each other in JS/TS.
// We call the version on the full mixin...
static restoreFromJSON(obj: any): MixinCore {
// ******************************************************
// * Key detail: Assumes an empty constructor is fine. *
// ******************************************************
return MixinCore._loadFromJSON(new MixinCore(), obj);
}
// Which, in turn, calls each constituent mixin (+ the base) version of the deserialization static.
static _loadFromJSON(target: MixinCore, obj: any): MixinCore {
// let the base load its stuff.
Base._loadFromJSON(target, obj);
// then load our stuff
target.counter = obj.counter;
// then return (in case another mixin has its own loading to do, too.)
return target;
};
public counterOver10(): boolean {
return this.counter > 10;
}
}
}
const funkyMixin = IntermediateMixin(Config);
const funkyDummy = new funkyMixin("funky", "dummy");
// const funkyDummy2 = new funkyMixin('a'); // errors on the extra param.
funkyMixin.restoreFromJSON;
// funkyDummy.restoreFromJSON; // error; is not an instance field/method
const reconstituted = funkyMixin.restoreFromJSON({
name: "foo",
title: "bar",
counter: 13
});
console.log(JSON.stringify(reconstituted, null, 2));
reconstituted.counter;
if(reconstituted.counterOver10()) {
console.log("`reconstituted`: success - counter is over 10");
}
// ---------
// function ExtraMixin<T extends AnyConstructor<ConfigImpl> & Omit<typeof Config, 'new'>>(Base: T) { // works!
function ExtraMixin<T extends AnyConstructor<ConfigImpl> & Omit<ConfigConstructor, 'new'>>(Base: T) {
return class ExMixinCore extends Base {
public flag: boolean = true;
// Utilizes the fact that constructors themselves inherit from each other in JS/TS.
// We call the version on the full mixin...
static restoreFromJSON(obj: any): ExMixinCore {
// Key detail: Assumes an empty constructor is fine.
return ExMixinCore._loadFromJSON(new ExMixinCore(), obj);
}
// Which, in turn, calls each constituent mixin (+ the base) version of the deserialization static.
static _loadFromJSON(target: ExMixinCore, obj: any): ExMixinCore {
// let the base load its stuff.
Base._loadFromJSON(target, obj);
// then load our stuff
target.flag = obj.flag;
// then return (in case another mixin has its own loading to do, too.)
return target;
};
}
}
// const stackedMixin = ExtraMixin(MixinWithStatics(funkyMixin));
// const stackedMixin = ExtraMixin<typeof funkyMixin>(MixinWithStatics(Config));
const stackedMixin = ExtraMixin(IntermediateMixin(Config));
const directBuilt = new stackedMixin('foo', 'bar');
directBuilt.counter;
directBuilt.flag;
// ******************************************************
// * Setup complete; now for limitations I discovered *
// ******************************************************
const reconstituted2 = stackedMixin.restoreFromJSON({
name: "foo",
title: "bar",
counter: 13,
flag: false
}); // if this replaces the following line, compilation errors arise on lines noted below.
// }) as InstanceType<typeof stackedMixin>; // Uncomment out the section to the left to replace the call's end, and things work!
let swappable = directBuilt;
// swappable = reconstituted2; // bombs - claims the intermediate mixin's properties don't exist.
let reverseSwappable = reconstituted2;
reverseSwappable = directBuilt; // passes swimmingly; no complaints here.
// Works regardless of the `reconstituted2` cast.
console.log(JSON.stringify(reconstituted2, null, 2));
reconstituted2.flag;
// Why doesn't TS recognize that these are available without the most recent cast
// (when assigning to `reconstituted2`)?
reconstituted2.counter; // error - `counter` undefined
if(reconstituted2.counterOver10() && !reconstituted2.flag) { // error - `counterOver10` undefined
console.log("`reconstituted`: success - counter is over 10 & flag is false");
}
// Aha! The deserialization pattern isn't quite getting resolved in full.
// But... shouldn't it resolve to the exact same type?
const cast = reconstituted2 as InstanceType<typeof stackedMixin>; // JUST a cast.
cast.counter;
cast.counterOver10;
// Works, but is redundant with the prior if-block.
// if(cast.counterOver10() && !cast.flag) {
// console.log("`cast`: success - counter is over 10 & flag is false");
// } I managed to get the mixins working with references to static methods declared by the base class; they even redirect to variants defined on intermediate mixins! The main issue - the final return value from the deserialization method has incomplete type inference in a manner very much in line with other cases I noticed on this issue. Related highlight from my example: function ExtraMixin<T extends AnyConstructor<ConfigImpl> & Omit<ConfigConstructor, 'new'>>(Base: T) {
return class ExMixinCore extends Base {
public flag: boolean = true;
// Utilizes the fact that constructors themselves inherit from each other in JS/TS.
// We call the version on the full mixin...
static restoreFromJSON(obj: any): ExMixinCore {
// Key detail: Assumes an empty constructor is fine.
return ExMixinCore._loadFromJSON(new ExMixinCore(), obj);
} Note: the static method is returning the same type as the mixin, based on the generic parameter The other main link that led me to this issue: #32080 (comment). I noticed that that comment's example Fortunately, it's possible to work around this oddity with a cast, but it does make the deserialization process a bit more verbose than desired. |
What I noticed there gave me an idea: what if the Well... it was worth a shot; we actually get more errors this way. But... those might be enlightening? // Was `ConfigConstructor`, but this is likely a better / more abstract name for what it seeks to represent.
interface ConstructorForRestorable<Type> {
restoreFromJSON: (obj: any) => Type;
_loadFromJSON: (target: Type, obj: any) => Type;
new (a: string, b: string /*...args: any[]*/): Type;
}
// ...
function IntermediateMixin<T extends AnyConstructor<ConfigImpl> & Omit<ConstructorForRestorable<T>, 'new'>>(Base: T) { // works!
return class MixinCore extends Base {
public counter: number = 0;
// Utilizes the fact that constructors themselves inherit from each other in JS/TS.
// We call the version on the full mixin...
static restoreFromJSON(obj: any): MixinCore {
// ******************************************************
// * Key detail: Assumes an empty constructor is fine. *
// ******************************************************
return MixinCore._loadFromJSON(new MixinCore(), obj);
}
// Which, in turn, calls each constituent mixin (+ the base) version of the deserialization static.
static _loadFromJSON(target: MixinCore, obj: any): MixinCore {
// let the base load its stuff.
Base._loadFromJSON(target, obj); // error squiggles appear here.
// then load our stuff
target.counter = obj.counter;
// then return (in case another mixin has its own loading to do, too.)
return target;
}; The error from the noted line:
Note: This is likely due to the generic-param This error and related behaviors also appear to prevent actually applying the mixin; the genericized class that props up the static method pseudo-inheritance is thus not viable. Error when attempting to use the mixin with this code variant:
That final line is a fun read. Essentially, "the type doesn't provide its own constructor"? Perhaps that's from the constructor-signature shenanigans I had to pull to meet the base mixin constructor requirements... except no, it persists even if that's the exact signature for the real constructor. |
@trusktr I think the problem with your initial example and the example in #32080 (comment) doesn't explicitly have to do with mixins: It's that you are using When Typescript merges the instance types of these constructors, everything is just getting collapsed into |
Search Terms
Suggestion
At the moment, it seems to be very difficult to compose mixins from other mixins.
Here's an example on StackOverflow: https://stackoverflow.com/questions/56680049
Here's an example on playground.
The code:
Use Cases
To make it simpler to make mixins (and compose them) like we can in plain JavaScript.
I'm porting JavaScript code to TypeScript, and the JavaScript makes great use of mixins (including composing new mixins from other mixins), but the composition ispractically impossible to do in TypeScript without very tedious type casting.
Examples
Here is the plain JS version of the above example:
It seems to me, that the type checker can realize that the class returned from
FooMixin(Base)
will be atypeof Foo
. The type system could at least be able to allow theBar
class to use methods and properties fromFoo
, despite not knowing what theBase
class will be.You can also imagine this problem gets worse with more composition, f.e.
It should also be possible to constrain the constructor to inherit from a certain base class. For example, the following doesn't work:
(EDIT: this part may actually be moved to a separate issue)
(EDIT 2: this part seems to be resolved)
playground link
As @dragomirtitian pointed out on SO, there are workarounds, but they appear to be very complicated and impractical.
Here's a more realistic example of what I'm doing in JS (and trying to port to TS): I'm using a
Mixin()
helper function, as a type declaration for the following example, which in practice implements things likeSymbol.hasInstance
to check if instances areinstanceof
a given mixin, prevents duplicate mixin applications, and other features, but the types don't work:playground link
Is there a way to do this currently, that we may have missed? (cc: @justinfagnani)
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: