Home > front end >  Typescript abstract class extend from mixin(class, generic) / mixin(class A, class B | class D...)
Typescript abstract class extend from mixin(class, generic) / mixin(class A, class B | class D...)

Time:11-04

I recently ran into a problem and I can't figure out the solution from other questions. So basically what I would like to achieve is:

I have a lot of abstract classes, and I have to create more abstract classes that extends from the others such as this:

abstract class MyAbstractClass extends Mixin(A, B | C | D)

where Mixin is a function from the ts-mixer npm package and A is an abstract class that won't change, it stays the same, however the other class is not known specifically.

I tried it with generics with no luck, I tried creating a factory function, but I can't return an abstract class like this

return abstract class MyAbstractClass extends Mixin(A, param){}

only a non-abstract class.

If somebody could provide an answer or point me in the right direction I would appreciate it

edit: What I'm currently doing is:

abstract class Button{
    abstract onToggle():void
}

abstract class A{
    abstract foo():void
}
abstract class B{
    abstract bar():void
}

abstract class ExtendedButtonA extends Mixin(Button, A){}
abstract class ExtendedButtonB extends Mixin(Button, B){}

So when I make the final class I have to do it like this:

class FinalButtonA extends ExtendedButtonA{}
class FinalButtonB extends ExtendedButtonB{}

What I want to be able to do:

class FinalButton extends ExtendedButton<A>{}

or something like this.

CodePudding user response:

My suggestion is write the ExtendedButton factory function like this:

function ExtendedButton<T extends abstract new (...args: any) => InstanceType<T>>(
  ctor: T
) {
  return Mixin(Button, ctor);
}

This seems to work for your example code:

abstract class A {
  abstract foo(): void
}

abstract class FinalButton extends ExtendedButton(A) { }

function foo(fb: FinalButton) {
  fb.foo();
  fb.onToggle();
}

The way I did this was to first see what happens when you call Mixin(Button, A) directly:

const testA = Mixin(Button, A)
// const testA: Class<any[], Button & A, typeof Button & typeof A, false>

And then compare it to a generic version where instead of A we pass in a ctor value of a generic abstract construct signature type:

function ExtendedButton<T extends abstract new (...args: any) => object>(
  ctor: T
) {
  return Mixin(Button, ctor);
}

const testA2 = ExtendedButton(A);
// const testA2: Class<any[], Button & object, typeof Button & typeof A, false>

The difference here is that the second type argument is Button & object instead of the desired Button & A. Why is that? It seems that the compiler is widening the constructor to its constraint abstract new (...args: any) => object and getting its instance type, prematurely widening all the way to object when we wanted A.

Well, if it's going to use that constraint, maybe we can make the constraint itself generic, so that it's still useful. Given a constructorlike type T, we can use the InstanceType<T> utility type to express its instance type. So let's change object to InstanceType<T>:

function ExtendedButton<T extends abstract new (...args: any) => InstanceType<T>>(
  ctor: T
) {
  return Mixin(Button, ctor);
}

Luckily that compiles (sometimes recursive constraints don't), and now the call signature looks like

// function ExtendedButton<T extends abstract new (...args: any) => InstanceType<T>>(
//   ctor: T
// ): Class<any[], Button & InstanceType<T>, typeof Button & T, false>

where the second type parameter is Button & InstanceType<T>. Let's try it out:

const testA3 = ExtendedButton(A);
// const testA3: Class<any[], Button & A, typeof Button & typeof A, false>

Perfect, that's the same type as what we get if we call Mixin directly.


Playground link to code

  • Related