Skip to content

[Regression] TS2562 - mixins cannot accept generic types #24122

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

Open
lifaon74 opened this issue May 15, 2018 · 7 comments
Open

[Regression] TS2562 - mixins cannot accept generic types #24122

lifaon74 opened this issue May 15, 2018 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@lifaon74
Copy link

lifaon74 commented May 15, 2018

TypeScript Version: 2.6.0 and more

Search Terms: mixin, 2.6.

Code

export interface Constructor<T = any> extends Function {
  new(...args: any[]): T;
}

class A<T> {
  public a: T;
}

class B<T> {
  public b: T;
}

function Mixin<T>(...classes: any[]): Constructor<T> {
  return function() {
    // whatever, not the purpose of the demonstration
  } as any;
}

interface IAB<T, U> extends A<T>, B<U> {}
// on typescript 2.6.0 and more, its no more possible to use generic types into mixins....
class AB<T, U> extends Mixin<IAB<T, U>>(A, B) {
}

// on typescript 2.6.0 we're forced to write
// class AB<T, U> extends Mixin<IAB<any, any>>(A, B) {}
//  so we loose type checking...

// intentional type error
class testClass extends AB<boolean, string> {
  public a: number; // before 2.6.0 => type of 'a' properly detected as wrong, boolean expected
}

Expected behavior:
Like before typescript 2.6.0, extending a class with a mixin which takes generic types should be allowed.
Using mixin is a really common usage in js/ts to build classes which implement/inherit properties from more than one classe (sometimes named 'factories'). Before 2.6.0, typescript properly detected the union of class having generic types, but currently this generates TS2562 errors, so its no more possible to construct typed generic mixins.

In a more generic way:

function A<T>() {
  return class {};
}

// should be allowed
class B<T> extends A<T>() {}

Actual behavior:
TS2562, Base class expression cannot reference class type parameters (on T and U)

Playground Link: here

Related Issues: Partially related #19668

@mhegazy mhegazy added the Suggestion An idea for TypeScript label Jul 19, 2018
@weswigham weswigham added the Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature label Nov 6, 2018
@lifaon74
Copy link
Author

lifaon74 commented Dec 5, 2018

So I saw recently this issue: #26154

The bug comes probably from here #17829 with this commit #17922 (@ahejlsberg)

@RyanCavanaugh explains that it is intentional but in fact it's a regression as everything was working as expected before the 2.6.0

The example I provided is a typical example and often used in mixin:

  • two generic classes (A and B)
  • a generic parent class that "inherit" (mixin) A and B (AB<T, U> extends A<T>, B<U>)

Currently it's no more possible to do class AB<T, U> extends mixin<T, U>(A, B){...} as mixin cannot handle generic types anymore...

My projects (and probably some of others developers) are stuck to pre 2.6.0 due to this regression. This would be super to re-enable this type of mixin 🥇

@lifaon74
Copy link
Author

lifaon74 commented Dec 5, 2018

Another pseudo example to understand the problem:

class B {
  b: number;
}

function CMixin<TBase extends Constructor<any>>(base: TBase) {
  // NOTE the generic type for the mixin class
  return class C<T> extends base {
    c: T;
    constructor(...args: any[]) {
      super(args);
    }
  }
}

// should have a way to provide T from A to the mixin
class A<T> extends <T>CMixin<Constructor<B>>(B) {
  constructor() {
    super();
  }
}

But I would prefer to be able to pass T directly to the mixin as it was allowed before CMixin<T>. This allow more customisation if the mixin is an implemementation in JS (ex: merge the prototypes vs class extend).

@th0r
Copy link

th0r commented May 17, 2019

Here is my simplified use-case:

function ContainerMixin<TModel>(baseClass: Constructor<any> = Object) {
  return class extends baseClass {
    model: TModel;
  };
}

interface ModelA {
  foo: string;
}

interface ModelB {
  bar: string;
}

// Throws an error "Base class expressions cannot reference class type parameters"
class Container<TModel> extends ContainerMixin<TModel>() {}

const containerForModelA = new Container<ModelA>();
// `model` property should have type `ModelA`
containerForModelA.model = {foo: 'bar'};
const containerForModelB = new Container<ModelB>();
// `model` property should have type `ModelB`
containerForModelB.model = {bar: 'baz'};

Currently I have to workaround it by implementing a container class for each type of model:

class ContainerForA extends ContainerMixin<ModelA>() {}
class ContainerForB extends ContainerMixin<ModelB>() {}

So it's not an ideal solution.

@web-padawan
Copy link

Are there any plans to change this behavior in future?

Currently it prevents using mixins with generic type arguments when creating web components. See example:

declare function ComboBoxMixin<P, T extends new (...args: any[]) => {}>(
  base: T
): T & ComboBoxMixinConstructor<P>;

interface ComboBoxMixinConstructor<T> {
  new (...args: any[]): ComboBoxMixin<T>;
}

interface ComboBoxMixin<T> {
  items: Array<T> | undefined;
}

// Error: "Base class expressions cannot reference class type parameters"
declare class ComboBoxElement<T> extends ComboBoxMixin<T>(HTMLElement) {
}

@lifaon74
Copy link
Author

I have found an elegant workaround to this problem: https://github.com/lifaon74/traits#assembletraitimplementations -> see Example: using a base class

I migrated the Generic from the mixin function to the new of the constructor

@ghost
Copy link

ghost commented Jan 22, 2021

@lifaon74
Could you provide a simple example of your approach here? I am a bit confused about the example given in the link (e.g., there is no mixin function, right?). Actually, I think your approach will run in error ts2545.

But I fully agree that this problem needs to be solved, otherwise mixins are largely useless for generic classes.

@lifaon74
Copy link
Author

@Remirror-zz

My mixin function looks like this:

interface MixedClasses<A, B> extends ClassA<A>, ClassB<B>, ... {
}

interface MixedClassesConstructor {
  new<A, B>(...args: any[]): MixedClasses<A, B>;
}

function mixin<GConstructor>(...classes): GConstructor {
 // code...
}

class ChildClass<A, B> extends mixin<MixedClassesConstructor>(ClassA, ClassB)<A, B> {
}

Note that the generics comes after the mixin call.

So yes, we loose the infer part of typescript (we need to specify everything => more verbose), but somehow it makes sense because writing mixin<A, B> means:

interface MixedClassesConstructor<A, B> {
  new(...args: any[]): MixedClasses<A, B>;
}

which is less correct (the generics should come after new)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants